stats/stats.go
Rama main · réplica de lectura del repositorio original.
package stats
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"os"
"runtime/pprof"
"strings"
"sync"
"time"
"github.com/coroot/coroot/auditor"
"github.com/coroot/coroot/cache"
cloud_pricing "github.com/coroot/coroot/cloud-pricing"
"github.com/coroot/coroot/constructor"
"github.com/coroot/coroot/db"
"github.com/coroot/coroot/model"
"github.com/coroot/coroot/timeseries"
"github.com/coroot/coroot/utils"
"github.com/gorilla/mux"
"github.com/grafana/pyroscope-go/godeltaprof"
"k8s.io/klog"
)
const (
collectUrl = "https://coroot.com/ce/usage-statistics"
collectInterval = time.Hour
sendTimeout = time.Minute
worldWindow = timeseries.Hour
)
type Stats struct {
Instance struct {
Uuid string `json:"uuid"`
Version string `json:"version"`
DatabaseType string `json:"database_type"`
Edition string `json:"edition"`
InstallationType string `json:"installation_type,omitempty"`
} `json:"instance"`
Integration struct {
Prometheus bool `json:"prometheus"`
PrometheusRefreshInterval int `json:"prometheus_refresh_interval"`
NodeAgent bool `json:"node_agent"`
NodeAgentVersions *utils.StringSet `json:"node_agent_versions"`
KubeStateMetrics *bool `json:"kube_state_metrics"`
InspectionOverrides map[model.CheckId]InspectionOverride `json:"inspection_overrides"`
ApplicationCategories int `json:"application_categories"`
AlertingIntegrations *utils.StringSet `json:"alerting_integrations"`
CloudCosts bool `json:"cloud_costs"`
Clickhouse bool `json:"clickhouse"`
Tracing bool `json:"tracing"`
Logs bool `json:"logs"`
Profiles bool `json:"profiles"`
JavaAsyncProfiler bool `json:"java_async_profiler"`
FluxCD bool `json:"fluxcd"`
ArgoCD bool `json:"argocd"`
} `json:"integration"`
Stack struct {
Clouds *utils.StringSet `json:"clouds"`
Services *utils.StringSet `json:"services"`
InstrumentedServices *utils.StringSet `json:"instrumented_services"`
} `json:"stack"`
Infra struct {
Projects int `json:"projects"`
Nodes int `json:"nodes"`
CPUCores int `json:"cpu_cores"`
GPUs int `json:"gpus"`
Applications int `json:"applications"`
ApplicationsByKind map[model.ApplicationKind]*AppStats `json:"applications_by_kind"`
Instances int `json:"instances"`
Deployments int `json:"deployments"`
DeploymentSummaries map[string]int `json:"deployment_summaries"`
KernelVersions *utils.StringSet `json:"kernel_versions"`
} `json:"infra"`
UX struct {
WorldLoadTimeAvg float32 `json:"world_load_time_avg"`
AuditTimeAvg float32 `json:"audit_time_avg"`
UsersByScreenSize map[string]int `json:"users_by_screen_size"`
UsersByTheme map[string]int `json:"users_by_theme"`
Users *utils.StringSet `json:"users"`
UsersByRole map[string]int `json:"users_by_role"`
PageViews map[string]int `json:"page_views"`
ApiCalls map[string]int `json:"api_calls"`
McpCalls map[string]int `json:"mcp_calls"`
SentNotifications map[db.IntegrationType]int `json:"sent_notifications"`
} `json:"ux"`
Performance struct {
Constructor constructor.Profile `json:"constructor"`
Auditor auditor.Profile `json:"auditor"`
Components []*Component `json:"components"`
} `json:"performance"`
Profile struct {
From int64 `json:"from"`
To int64 `json:"to"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
} `json:"profile"`
}
type AppStats struct {
Applications int `json:"applications"`
Instances int `json:"instances"`
}
type Component struct {
Id model.ApplicationId `json:"id"`
Instances []*Instance `json:"instances"`
}
type Instance struct {
Containers map[string]*Container `json:"containers"`
Volumes []*Volume `json:"volumes"`
}
type Container struct {
CpuTotal timeseries.Value `json:"cpu_total"`
CpuUsage timeseries.Value `json:"cpu_usage"`
CpuLimit timeseries.Value `json:"cpu_limit"`
CpuDelay timeseries.Value `json:"cpu_delay"`
CpuThrottling timeseries.Value `json:"cpu_throttling"`
MemoryTotal timeseries.Value `json:"memory_total"`
MemoryUsage timeseries.Value `json:"memory_usage"`
MemoryLimit timeseries.Value `json:"memory_limit"`
MemoryOOMs timeseries.Value `json:"memory_ooms"`
Restarts timeseries.Value `json:"restarts"`
}
type Volume struct {
Size timeseries.Value `json:"size"`
Usage timeseries.Value `json:"usage"`
ReadLatency timeseries.Value `json:"read_latency"`
WriteLatency timeseries.Value `json:"write_latency"`
Reads timeseries.Value `json:"reads"`
Writes timeseries.Value `json:"writes"`
ReadBandwidth timeseries.Value `json:"read_bandwidth"`
WriteBandwidth timeseries.Value `json:"write_bandwidth"`
}
type InspectionOverride struct {
ProjectLevel int `json:"project_level"`
ApplicationLevel int `json:"application_level"`
}
type Collector struct {
db *db.DB
cache *cache.Cache
pricing *cloud_pricing.Manager
client *http.Client
disabled bool
instanceUuid string
instanceVersion string
edition string
installationType string
usersByScreenSize map[string]*utils.StringSet
usersByTheme map[string]*utils.StringSet
pageViews map[string]int
apiCalls map[string]int
mcpCalls map[string]int
lock sync.Mutex
heapProfiler *godeltaprof.HeapProfiler
globalClickHouse *db.IntegrationClickhouse
}
func NewCollector(disabled bool, instanceUuid, version string, edition string, db *db.DB, cache *cache.Cache, pricing *cloud_pricing.Manager, globalClickHouse *db.IntegrationClickhouse) *Collector {
c := &Collector{
db: db,
cache: cache,
pricing: pricing,
client: &http.Client{Timeout: sendTimeout},
instanceUuid: instanceUuid,
instanceVersion: version,
edition: edition,
installationType: os.Getenv("INSTALLATION_TYPE"),
usersByScreenSize: map[string]*utils.StringSet{},
usersByTheme: map[string]*utils.StringSet{},
pageViews: map[string]int{},
apiCalls: map[string]int{},
mcpCalls: map[string]int{},
heapProfiler: godeltaprof.NewHeapProfiler(),
globalClickHouse: globalClickHouse,
disabled: disabled,
}
if err := c.heapProfiler.Profile(io.Discard); err != nil {
klog.Warningln(err)
}
if !c.disabled {
go func() {
c.send()
ticker := time.NewTicker(collectInterval)
for range ticker.C {
c.send()
}
}()
}
return c
}
type Event struct {
Type string `json:"type"`
DeviceId string `json:"device_id"`
DeviceSize string `json:"device_size"`
Path string `json:"path"`
Theme string `json:"theme"`
}
func (c *Collector) Stats(r *http.Request, w http.ResponseWriter) {
utils.WriteJson(w, c.collect())
}
func (c *Collector) MiddleWare(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
if route != nil {
pathTemplate, _ := route.GetPathTemplate()
if strings.HasPrefix(pathTemplate, "/api/") {
vars := mux.Vars(r)
if v := vars["view"]; v != "" {
pathTemplate = strings.ReplaceAll(pathTemplate, "{view}", v)
}
c.lock.Lock()
c.apiCalls[pathTemplate]++
c.lock.Unlock()
}
}
next.ServeHTTP(w, r)
})
}
func (c *Collector) RegisterMCPCall(tool string) {
if c == nil {
return
}
c.lock.Lock()
c.mcpCalls[tool]++
c.lock.Unlock()
}
func (c *Collector) RegisterRequest(r *http.Request) {
if c == nil {
return
}
var e Event
if err := utils.ReadJson(r, &e); err != nil {
klog.Warningln(err)
return
}
if e.DeviceId == "" || e.DeviceSize == "" {
return
}
c.lock.Lock()
defer c.lock.Unlock()
if e.Type == "route-open" {
c.pageViews[e.Path]++
}
if c.usersByScreenSize[e.DeviceSize] == nil {
c.usersByScreenSize[e.DeviceSize] = utils.NewStringSet()
}
c.usersByScreenSize[e.DeviceSize].Add(e.DeviceId)
if c.usersByTheme[e.Theme] == nil {
c.usersByTheme[e.Theme] = utils.NewStringSet()
}
c.usersByTheme[e.Theme].Add(e.DeviceId)
}
func (c *Collector) send() {
buf := new(bytes.Buffer)
if err := pprof.StartCPUProfile(buf); err != nil {
klog.Warningln(err)
}
from := time.Now()
stats := c.collect()
stats.Profile.From = from.Unix()
stats.Profile.To = time.Now().Unix()
pprof.StopCPUProfile()
stats.Profile.CPU = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
if err := c.heapProfiler.Profile(buf); err != nil {
klog.Warningln(err)
}
stats.Profile.Memory = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
if err := json.NewEncoder(buf).Encode(stats); err != nil {
klog.Errorln("failed to encode stats:", err)
return
}
res, err := c.client.Post(collectUrl, "application/json", buf)
if err != nil {
klog.Errorln("failed to send stats:", err)
return
}
_ = res.Body.Close()
}
func (c *Collector) collect() Stats {
stats := Stats{}
stats.Instance.Uuid = c.instanceUuid
stats.Instance.Version = c.instanceVersion
stats.Instance.DatabaseType = string(c.db.Type())
stats.Instance.Edition = c.edition
stats.Instance.InstallationType = c.installationType
stats.UX.UsersByScreenSize = map[string]int{}
stats.UX.Users = utils.NewStringSet()
c.lock.Lock()
stats.UX.PageViews = c.pageViews
c.pageViews = map[string]int{}
stats.UX.ApiCalls = c.apiCalls
c.apiCalls = map[string]int{}
stats.UX.McpCalls = c.mcpCalls
c.mcpCalls = map[string]int{}
for size, us := range c.usersByScreenSize {
stats.UX.UsersByScreenSize[size] = us.Len()
stats.UX.Users.Add(us.Items()...)
}
c.usersByScreenSize = map[string]*utils.StringSet{}
stats.UX.UsersByTheme = map[string]int{}
for theme, us := range c.usersByTheme {
stats.UX.UsersByTheme[theme] = us.Len()
}
c.usersByTheme = map[string]*utils.StringSet{}
c.lock.Unlock()
users, err := c.db.GetUsers()
if err != nil {
klog.Errorln("failed to get users:", err)
}
stats.UX.UsersByRole = map[string]int{}
for _, u := range users {
for _, r := range u.Roles {
stats.UX.UsersByRole[string(r)]++
}
}
projects, err := c.db.GetProjects()
if err != nil {
klog.Errorln("failed to get projects:", err)
return stats
}
stats.Infra.Projects = len(projects)
applicationCategories := utils.NewStringSet()
stats.Integration.NodeAgentVersions = utils.NewStringSet()
stats.Infra.KernelVersions = utils.NewStringSet()
stats.Integration.InspectionOverrides = map[model.CheckId]InspectionOverride{}
stats.Integration.AlertingIntegrations = utils.NewStringSet()
stats.Stack.Clouds = utils.NewStringSet()
stats.Stack.Services = utils.NewStringSet()
stats.Stack.InstrumentedServices = utils.NewStringSet()
stats.Performance.Constructor.Stages = map[string]float32{}
stats.Performance.Constructor.Queries = map[string]constructor.QueryStats{}
stats.Infra.DeploymentSummaries = map[string]int{}
stats.Infra.ApplicationsByKind = map[model.ApplicationKind]*AppStats{}
var loadTime, auditTime []time.Duration
now := timeseries.Now()
for _, p := range projects {
if p.Multicluster() {
continue
}
if p.Prometheus.Url != "" {
stats.Integration.Prometheus = true
}
if stats.Integration.PrometheusRefreshInterval == 0 || int(p.Prometheus.RefreshInterval) < stats.Integration.PrometheusRefreshInterval {
stats.Integration.PrometheusRefreshInterval = int(p.Prometheus.RefreshInterval)
}
if cfg := p.ClickHouseConfig(c.globalClickHouse); cfg != nil && cfg.Addr != "" {
stats.Integration.Clickhouse = true
stats.Integration.Tracing = true
stats.Integration.Logs = true
stats.Integration.Profiles = true
}
for category := range p.Settings.ApplicationCategorySettings {
applicationCategories.Add(string(category))
}
for _, i := range p.Settings.Integrations.GetInfo() {
if i.Configured {
stats.Integration.AlertingIntegrations.Add(string(i.Type))
}
}
checkConfigs, err := c.db.GetCheckConfigs(p.Id)
if err != nil {
klog.Errorln(err)
continue
}
for appId, configs := range checkConfigs {
for checkId := range configs {
s := stats.Integration.InspectionOverrides[checkId]
if appId.IsZero() {
s.ProjectLevel++
} else {
s.ApplicationLevel++
}
stats.Integration.InspectionOverrides[checkId] = s
}
}
cacheClient := c.cache.GetCacheClient(p.Id)
cacheTo, err := cacheClient.GetTo()
if err != nil {
klog.Errorln(err)
continue
}
if cacheTo.IsZero() || cacheTo.Before(now.Add(-worldWindow)) {
continue
}
t := time.Now()
to := cacheTo
from := to.Add(-worldWindow)
step, err := cacheClient.GetStep(from, to)
if err != nil {
klog.Errorln(err)
continue
}
ctr := constructor.New(c.db, p, map[db.ProjectId]constructor.Cache{p.Id: cacheClient}, c.pricing)
w, err := ctr.LoadWorld(context.Background(), from, to, step, &stats.Performance.Constructor)
if err != nil {
klog.Errorln("failed to load world:", err)
continue
}
loadTime = append(loadTime, time.Since(t))
t = time.Now()
auditor.Audit(w, p, nil, &stats.Performance.Auditor)
auditTime = append(auditTime, time.Since(t))
stats.Integration.NodeAgent = stats.Integration.NodeAgent || w.IntegrationStatus.NodeAgent.Installed
if w.IntegrationStatus.KubeStateMetrics.Required {
installed := w.IntegrationStatus.KubeStateMetrics.Installed
if stats.Integration.KubeStateMetrics != nil {
installed = *stats.Integration.KubeStateMetrics || installed
}
stats.Integration.KubeStateMetrics = &installed
}
if w.Flux != nil {
stats.Integration.FluxCD = true
}
if w.ArgoCD != nil {
stats.Integration.ArgoCD = true
}
stats.Infra.Nodes += len(w.Nodes)
for _, n := range w.Nodes {
if cores := n.CpuCapacity.Last(); cores > 0 {
stats.Infra.CPUCores += int(cores)
}
stats.Infra.GPUs += len(n.GPUs)
stats.Integration.NodeAgentVersions.Add(n.AgentVersion.Value())
stats.Infra.KernelVersions.Add(n.KernelVersion.Value())
stats.Stack.Clouds.Add(strings.ToLower(n.CloudProvider.Value()))
if n.Price != nil {
stats.Integration.CloudCosts = true
}
}
for _, a := range w.Applications {
as := stats.Infra.ApplicationsByKind[a.Id.Kind]
if as == nil {
as = &AppStats{}
stats.Infra.ApplicationsByKind[a.Id.Kind] = as
}
as.Applications++
as.Instances += len(a.Instances)
if a.IsStandalone() || a.Category.Auxiliary() {
continue
}
stats.Infra.Applications++
stats.Infra.Instances += len(a.Instances)
for _, i := range a.Instances {
for t := range i.ApplicationTypes() {
stats.Stack.Services.Add(string(t))
}
stats.Stack.InstrumentedServices.Add(string(i.InstrumentedType()))
if !stats.Integration.JavaAsyncProfiler {
for _, j := range i.Jvms {
if j.ProfilingEnabled {
stats.Integration.JavaAsyncProfiler = true
break
}
}
}
}
for _, ds := range model.CalcApplicationDeploymentStatuses(a, w.CheckConfigs, now) {
if now.Sub(ds.Deployment.StartedAt) > timeseries.Hour {
continue
}
stats.Infra.Deployments++
for _, s := range ds.Summary {
sign := "-"
if s.Ok {
sign = "+"
}
stats.Infra.DeploymentSummaries[sign+string(s.Report)]++
}
}
}
stats.Performance.Components = append(stats.Performance.Components, corootComponents(w.GetCorootComponents())...)
}
stats.Integration.ApplicationCategories = applicationCategories.Len()
stats.UX.WorldLoadTimeAvg = avgDuration(loadTime)
stats.UX.AuditTimeAvg = avgDuration(auditTime)
stats.UX.SentNotifications = c.db.GetSentIncidentNotificationsStat(now.Add(-timeseries.Duration(collectInterval.Seconds())))
return stats
}
func corootComponents(components []*model.Application) []*Component {
var res []*Component
for _, a := range components {
aa := &Component{Id: a.Id}
res = append(res, aa)
for _, i := range a.Instances {
if i.IsObsolete() {
continue
}
ii := &Instance{Containers: map[string]*Container{}}
aa.Instances = append(aa.Instances, ii)
for _, c := range i.Containers {
if c.InitContainer {
continue
}
cc := &Container{}
ii.Containers[c.Name] = cc
cc.CpuLimit = timeseries.Value(c.CpuLimit.Last())
cc.CpuUsage = timeseries.Value(c.CpuUsage.Last())
cc.CpuDelay = timeseries.Value(c.CpuDelay.Last())
cc.CpuThrottling = timeseries.Value(c.ThrottledTime.Last())
cc.MemoryLimit = timeseries.Value(c.MemoryLimit.Last())
cc.MemoryUsage = timeseries.Value(c.MemoryRss.Last())
cc.MemoryOOMs = timeseries.Value(c.OOMKills.Reduce(timeseries.NanSum))
cc.Restarts = timeseries.Value(c.Restarts.Reduce(timeseries.NanSum))
if i.Node != nil {
cc.CpuTotal = timeseries.Value(i.Node.CpuCapacity.Last())
cc.MemoryTotal = timeseries.Value(i.Node.MemoryTotalBytes.Last())
}
}
for _, v := range i.Volumes {
vv := &Volume{}
ii.Volumes = append(ii.Volumes, vv)
vv.Size = timeseries.Value(v.CapacityBytes.Last())
vv.Usage = timeseries.Value(v.UsedBytes.Last())
if i.Node != nil {
if d := i.Node.Disks[v.Device.Value()]; d != nil {
vv.ReadLatency = timeseries.Value(d.ReadTime.Last())
vv.WriteLatency = timeseries.Value(d.WriteTime.Last())
vv.Reads = timeseries.Value(d.ReadOps.Last())
vv.Writes = timeseries.Value(d.WriteOps.Last())
vv.ReadBandwidth = timeseries.Value(d.ReadBytes.Last())
vv.WriteBandwidth = timeseries.Value(d.WrittenBytes.Last())
}
}
}
}
}
return res
}
func avgDuration(durations []time.Duration) float32 {
if len(durations) == 0 {
return 0
}
var total time.Duration
for _, d := range durations {
total += d
}
return float32(total.Seconds() / float64(len(durations)))
}