package model import ( "net" "sort" "strconv" "github.com/coroot/coroot/timeseries" "github.com/coroot/coroot/utils" "golang.org/x/exp/maps" ) type ClusterManager string const ( ClusterManagerCNPG ClusterManager = "cnpg" ClusterManagerCrunchy ClusterManager = "crunchy" ClusterManagerZalando ClusterManager = "zalando" ClusterManagerStackGres ClusterManager = "stackgres" ClusterManagerPerconaMongoDB ClusterManager = "percona-mongodb" ClusterManagerPerconaXtraDB ClusterManager = "percona-xtradb" ClusterManagerPerconaServer ClusterManager = "percona-server-mysql" ) type ApplicationCluster struct { Manager ClusterManager Backups *DBBackups } type Application struct { Id ApplicationId Custom bool Category ApplicationCategory Cluster ApplicationCluster Annotations ApplicationAnnotations Instances []*Instance instancesByName map[string][]*Instance Downstreams map[ApplicationId]*AppToAppConnection Upstreams map[ApplicationId]*AppToAppConnection TrafficStats TrafficStats DesiredInstances *timeseries.TimeSeries LatencySLIs []*LatencySLI AvailabilitySLIs []*AvailabilitySLI LLMLatencySLI *LatencySLI // latency of inbound LLM inference requests, nil if the app doesn't serve them Events []*ApplicationEvent Deployments []*ApplicationDeployment Incidents []*ApplicationIncident LogMessages map[Severity]*LogMessages Status Status Reports []*AuditReport Settings *ApplicationSettings KubernetesServices []*Service DNSRequests map[DNSRequest]map[string]*timeseries.TimeSeries DNSRequestsHistogram map[float32]*timeseries.TimeSeries InboundRequestsCount map[string]*timeseries.TimeSeries InboundRequestsHistogram map[float32]*timeseries.TimeSeries InboundLLMRequestsHistogram map[float32]*timeseries.TimeSeries PeriodicSystemdJob bool } func NewApplication(id ApplicationId) *Application { app := &Application{ Id: id, Annotations: ApplicationAnnotations{}, instancesByName: map[string][]*Instance{}, LogMessages: map[Severity]*LogMessages{}, Upstreams: map[ApplicationId]*AppToAppConnection{}, Downstreams: map[ApplicationId]*AppToAppConnection{}, DNSRequests: map[DNSRequest]map[string]*timeseries.TimeSeries{}, DNSRequestsHistogram: map[float32]*timeseries.TimeSeries{}, InboundRequestsCount: map[string]*timeseries.TimeSeries{}, InboundRequestsHistogram: map[float32]*timeseries.TimeSeries{}, InboundLLMRequestsHistogram: map[float32]*timeseries.TimeSeries{}, } return app } func (app *Application) SLOStatus() Status { for _, r := range app.Reports { if r.Name == AuditReportSLO { return r.Status } } return UNKNOWN } func (app *Application) OtelLogService(candidates []string, w *World) string { if app.Settings != nil && app.Settings.Logs != nil { return app.Settings.Logs.Service } return GuessService(candidates, w, app) } func (app *Application) LogQueryServices(source LogSource, otelService string) []string { switch source { case LogSourceOtel: if otelService != "" { return []string{otelService} } case LogSourceAgent: return app.LogServices() } return nil } func (app *Application) LogServices() []string { res := utils.NewStringSet() for _, i := range app.Instances { for _, c := range i.Containers { res.Add(ContainerIdToServiceName(c.Id)) } if i.Rds != nil && i.Rds.Id != "" { res.Add("/aws/rds/" + i.Rds.Id) } if i.Elasticache != nil && i.Elasticache.Id != "" { res.Add("/aws/elasticache/" + i.Elasticache.Id) } if i.CloudSQL != nil && i.CloudSQL.Id != "" { res.Add("/gcp/cloudsql/" + i.CloudSQL.Id) } if i.Memorystore != nil && i.Memorystore.Id != "" { res.Add("/gcp/memorystore/" + i.Memorystore.Id) } if i.OCIDB != nil && i.OCIDB.Id != "" { res.Add("/oci/db/" + i.OCIDB.Id) } if i.OCICache != nil && i.OCICache.Id != "" { res.Add("/oci/cache/" + i.OCICache.Id) } } return res.Items() } func (app *Application) SimilarLogPatternHashes(hash string) []string { res := utils.NewStringSet(hash) for _, msgs := range app.LogMessages { for _, pattern := range msgs.Patterns { if similar := pattern.SimilarPatternHashes; similar != nil { if similar.Has(hash) { res.Add(similar.Items()...) } } } } return res.Items() } func (app *Application) GetInstance(name, node string) *Instance { for _, i := range app.instancesByName[name] { switch app.Id.Kind { case ApplicationKindStatefulSet: if node == "" || i.NodeName() == "" || i.NodeName() == node { return i } default: return i } } return nil } func (app *Application) GetOrCreateInstance(name string, node *Node) *Instance { nodeName := "" if node != nil { nodeName = node.GetName() } instance := app.GetInstance(name, nodeName) if instance == nil { instance = NewInstance(name, app) app.Instances = append(app.Instances, instance) app.instancesByName[name] = append(app.instancesByName[name], instance) if node != nil { instance.Node = node node.Instances = append(node.Instances, instance) } } return instance } func (app *Application) Labels() Labels { res := Labels{} switch app.Id.Kind { case ApplicationKindRds: res["db"] = app.Instances[0].Rds.Engine.Value() + " (RDS)" case ApplicationKindElasticacheCluster: res["db"] = app.Instances[0].Elasticache.Engine.Value() + " (EC)" case ApplicationKindCloudSQL: res["db"] = app.Instances[0].CloudSQL.Engine.Value() + " (Cloud SQL)" case ApplicationKindMemorystore: res["db"] = app.Instances[0].Memorystore.Engine.Value() + " (Memorystore)" case ApplicationKindOCIDB: res["db"] = app.Instances[0].OCIDB.Engine.Value() + " (OCI)" case ApplicationKindOCICache: res["db"] = app.Instances[0].OCICache.Engine.Value() + " (OCI Cache)" case ApplicationKindUnknown, ApplicationKindDockerSwarmService, ApplicationKindNomadJobGroup, ApplicationKindECSService: if app.Id.Namespace != "_" { res["ns"] = app.Id.Namespace } else { res["instances"] = strconv.Itoa(len(app.Instances)) } case ApplicationKindExternalService: eps := utils.NewStringSet() for _, instance := range app.Instances { for listen := range instance.TcpListens { eps.Add(listen.IP) } } if eps.Len() > 0 { name := "external endpoint" value := eps.Items()[0] if eps.Len() > 1 { name += "s" value += ",..." } res[name] = value } default: res["ns"] = app.Id.Namespace } for _, i := range app.Instances { for _, c := range i.Containers { for t := range c.ApplicationTypes { if t.IsDatabase() { res["db"] = string(t) } if t.IsQueue() { res["queue"] = string(t) } } } } return res } func (app *Application) IsRedis() bool { for _, i := range app.Instances { if i.Redis != nil { return true } } return false } func (app *Application) IsMongodb() bool { for _, i := range app.Instances { if i.Mongodb != nil { return true } } return false } func (app *Application) IsMysql() bool { for _, i := range app.Instances { if i.Mysql != nil { return true } } return false } func (app *Application) IsMemcached() bool { for _, i := range app.Instances { if i.Memcached != nil { return true } } return false } func (app *Application) IsPostgres() bool { for _, i := range app.Instances { if i.Postgres != nil { return true } } return false } func (app *Application) IsJvm() bool { for _, i := range app.Instances { if len(i.Jvms) > 0 { return true } } return false } func (app *Application) IsDotNet() bool { for _, i := range app.Instances { if len(i.DotNet) > 0 { return true } } return false } func (app *Application) IsPython() bool { for _, i := range app.Instances { if i.Python != nil { return true } } return false } func (app *Application) IsNodejs() bool { for _, i := range app.Instances { if i.Nodejs != nil { return true } } return false } func (app *Application) IsStandalone() bool { for _, d := range app.Downstreams { if d.Application != d.RemoteApplication { return false } } for _, u := range app.Upstreams { if u.Application != u.RemoteApplication { return false } } return true } func (app *Application) IsDatabase() bool { switch app.Id.Kind { case ApplicationKindRds, ApplicationKindElasticacheCluster, ApplicationKindCloudSQL, ApplicationKindMemorystore, ApplicationKindOCIDB, ApplicationKindOCICache: return true } for t := range app.ApplicationTypes() { if t.IsDatabase() { return true } } return false } func (app *Application) IsQueue() bool { for t := range app.ApplicationTypes() { if t.IsQueue() { return true } } return false } func (app *Application) IsK8s() bool { switch app.Id.Kind { case ApplicationKindCronJob, ApplicationKindJob, ApplicationKindDeployment, ApplicationKindDaemonSet, ApplicationKindPod, ApplicationKindReplicaSet, ApplicationKindStatefulSet, ApplicationKindStaticPods, ApplicationKindArgoWorkflow, ApplicationKindSparkApplication, ApplicationKindGitLabJob, ApplicationKindInferenceService, ApplicationKindDynamoGraphDeployment, ApplicationKindDisaggregatedSet, ApplicationKindLeaderWorkerSet, ApplicationKindSandbox: return true } for _, i := range app.Instances { if i.Pod != nil { return true } } return false } func (app *Application) hasClientsInAWS() bool { for _, d := range app.Downstreams { for _, i := range d.Application.Instances { if i.Node != nil { provider := i.Node.CloudProvider.Value() if provider == CloudProviderAWS { return true } } } } return false } func (app *Application) AddReport(name AuditReportName, widgets ...*Widget) { app.Reports = append(app.Reports, &AuditReport{Name: name, Widgets: widgets}) } func (app *Application) ApplicationTypes() map[ApplicationType]bool { res := map[ApplicationType]bool{} for _, i := range app.Instances { for t := range i.ApplicationTypes() { res[t] = true } } if app.Id.Kind == ApplicationKindExternalService { for _, d := range app.Downstreams { for _, ep := range d.Endpoints.Items() { _, port, _ := net.SplitHostPort(ep) var t ApplicationType switch port { case "5432": t = ApplicationTypePostgres case "3306": t = ApplicationTypeMysql case "6379": t = ApplicationTypeRedis case "11211": t = ApplicationTypeMemcached case "27017", "27018": t = ApplicationTypeMongodb } if t != "" { res[t] = true } } for p := range d.RequestsCount { t := p.ToApplicationType() if t == ApplicationTypeUnknown { continue } res[t] = true } } } return res } func (app *Application) ApplicationType() ApplicationType { types := maps.Keys(app.ApplicationTypes()) if len(types) == 0 { return ApplicationTypeUnknown } if len(types) == 1 { return types[0] } sort.Slice(types, func(i, j int) bool { ti, tj := types[i], types[j] tiw, tjw := ti.Weight(), tj.Weight() if tiw == tjw { return ti < tj } return tiw < tjw }) return types[0] } func (app *Application) PeriodicJob() bool { switch app.Id.Kind { case ApplicationKindJob, ApplicationKindCronJob, ApplicationKindGitLabJob, ApplicationKindSandbox: return true } return app.PeriodicSystemdJob } func (app *Application) AlertingDisabled() bool { switch app.Id.Kind { case ApplicationKindGitLabJob, ApplicationKindStaticPods, ApplicationKindCronJob, ApplicationKindJob, ApplicationKindArgoWorkflow, ApplicationKindSandbox: return true } return false } func (app *Application) WindowsApp() bool { for _, i := range app.Instances { if i.Node != nil && i.Node.GetOS() == OSWindows { return true } } return false } func (app *Application) ListensTCP() bool { for _, i := range app.Instances { for l := range i.TcpListens { if l.Port != "0" { return true } } } return false } func (app *Application) IsCorootComponent() bool { for t := range app.ApplicationTypes() { if t.IsCorootComponent() { return true } } return false } func (app *Application) GetCheckWithReport(checkId CheckId) (*Check, AuditReportName) { for _, r := range app.Reports { for _, ch := range r.Checks { if ch.Id == checkId { return ch, r.Name } } } return nil, "" }