happyDomain/internal/app/insights.go

204 lines
5 KiB
Go

// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package app
import (
"bytes"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"runtime"
"runtime/debug"
"strings"
"time"
"git.happydns.org/happyDomain/internal/api/controller"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
)
const (
InsightsUpdateInterval = 24 * time.Hour
InsightsEndpoint = "https://insights.happydomain.org/collect"
)
type insightsCollector struct {
cfg *happydns.Options
store storage.Storage
stop chan bool
}
func (c *insightsCollector) Close() {
c.stop <- true
}
func (c *insightsCollector) Run() {
if t, ok := c.LastRun(); !ok {
select {
case <-time.After(time.Hour):
break
case <-c.stop:
return
}
} else {
select {
case <-time.After(time.Until(t.Add(InsightsUpdateInterval))):
break
case <-c.stop:
return
}
}
var err error
for {
err = c.send()
if err != nil {
log.Println("Unable to send insights:", err.Error())
}
select {
case <-time.After(InsightsUpdateInterval + time.Duration(rand.Int31n(600000)-300000)*time.Microsecond):
continue
case <-c.stop:
return
}
}
}
func (c *insightsCollector) LastRun() (time.Time, bool) {
timestamp, _, err := c.store.LastInsightsRun()
if err != nil || timestamp == nil {
return time.Time{}, false
}
return *timestamp, true
}
func (c *insightsCollector) collect() (*happydns.Insights, error) {
_, instance, _ := c.store.LastInsightsRun()
// Basic info
data := happydns.Insights{
InsightsID: instance.String(),
Version: controller.HDVersion,
}
// Build info
data.Build.Settings, data.Build.GoVersion = buildInfo()
// OS info
data.OS.Type = runtime.GOOS
data.OS.Arch = runtime.GOARCH
data.OS.NumCPU = runtime.NumCPU()
// Config info
data.Config.DisableEmbeddedLogin = c.cfg.DisableEmbeddedLogin
data.Config.DisableProviders = c.cfg.DisableProviders
data.Config.DisableRegistration = c.cfg.DisableRegistration
data.Config.HasBaseURL = c.cfg.BasePath != ""
data.Config.HasDevProxy = c.cfg.DevProxy != ""
data.Config.HasExternalAuth = c.cfg.ExternalAuth.String() != ""
data.Config.HasListmonkURL = c.cfg.ListmonkURL.String() != ""
data.Config.LocalBind = strings.HasPrefix(c.cfg.Bind, "127.0.0.1:") || strings.HasPrefix(c.cfg.Bind, "[::1]:")
data.Config.NbOidcProviders = len(c.cfg.OIDCClients)
data.Config.NoAuthActive = c.cfg.NoAuth
data.Config.NoMail = c.cfg.NoMail
data.Config.NonUnixAdminBind = strings.Contains(c.cfg.AdminBind, ":")
data.Config.StorageEngine = string(c.cfg.StorageEngine)
// Database info
data.Database.Version = c.store.SchemaVersion()
if authusers, err := c.store.ListAllAuthUsers(); err != nil {
return nil, err
} else {
for authusers.Next() {
data.Database.NbAuthUsers++
}
}
users, err := c.store.ListAllUsers()
if err != nil {
return nil, err
}
data.Database.Providers = map[string]int{}
for users.Next() {
data.Database.NbUsers++
user := users.Item()
if providers, err := c.store.ListProviders(user); err == nil {
for _, provider := range providers {
data.Database.Providers[provider.Type] += 1
}
}
if domains, err := c.store.ListDomains(user); err == nil {
data.Database.NbDomains += len(domains)
for _, domain := range domains {
data.Database.NbZones += len(domain.ZoneHistory)
}
}
}
return &data, nil
}
func (c *insightsCollector) send() error {
data, err := c.collect()
if err != nil {
return err
}
dataenc, err := json.Marshal(data)
if err != nil {
return err
}
resp, err := http.Post(InsightsEndpoint, "application/json", bytes.NewReader(dataenc))
if err != nil {
return fmt.Errorf("could not send insights: %w", err)
}
resp.Body.Close()
log.Println("Sent insights data, status:", resp.Status)
return c.store.InsightsRun()
}
func buildInfo() (map[string]string, string) {
bInfo := map[string]string{}
var version string
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Value == "" {
continue
}
bInfo[setting.Key] = setting.Value
}
version = info.GoVersion
}
return bInfo, version
}