From f97e4cdedf9557ff0309e002b38c97d5d69af33a Mon Sep 17 00:00:00 2001 From: Nikolay Sivko Date: Fri, 19 Sep 2025 12:18:18 +0300 Subject: [PATCH] add a Node.js inspection that tracks event loop blocked time --- auditor/auditor.go | 1 + auditor/nodejs.go | 26 +++++++++++++ constructor/constructor.go | 1 + constructor/nodejs.go | 24 ++++++++++++ constructor/queries.go | 1 + model/application.go | 9 +++++ model/application_types.go | 2 + model/audit_report.go | 1 + model/check.go | 79 +++++++++++++++++++++----------------- model/instance.go | 1 + model/nodejs.go | 7 ++++ 11 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 auditor/nodejs.go create mode 100644 constructor/nodejs.go create mode 100644 model/nodejs.go diff --git a/auditor/auditor.go b/auditor/auditor.go index 830d63972..b998d63a2 100644 --- a/auditor/auditor.go +++ b/auditor/auditor.go @@ -66,6 +66,7 @@ func Audit(w *model.World, p *db.Project, generateDetailedReportFor *model.Appli stages.stage("jvm", a.jvm) stages.stage("dotnet", a.dotnet) stages.stage("python", a.python) + stages.stage("nodejs", a.nodejs) stages.stage("logs", a.logs) stages.stage("deployments", a.deployments) diff --git a/auditor/nodejs.go b/auditor/nodejs.go new file mode 100644 index 000000000..3dc88c0ad --- /dev/null +++ b/auditor/nodejs.go @@ -0,0 +1,26 @@ +package auditor + +import ( + "github.com/coroot/coroot/model" +) + +func (a *appAuditor) nodejs() { + if !a.app.IsNodejs() { + return + } + + report := a.addReport(model.AuditReportNodejs) + check := report.CreateCheck(model.Checks.NodejsEventLoopBlockedTime) + chart := report.GetOrCreateChart("Node.js event loop blocked time, seconds/second", nil) + for _, i := range a.app.Instances { + if i.Nodejs == nil { + continue + } + if i.Nodejs.EventLoopBlockedTime.Last() > check.Threshold { + check.AddItem(i.Name) + } + if chart != nil { + chart.AddSeries(i.Name, i.Nodejs.EventLoopBlockedTime) + } + } +} diff --git a/constructor/constructor.go b/constructor/constructor.go index 29fbf0e7e..945e560d0 100644 --- a/constructor/constructor.go +++ b/constructor/constructor.go @@ -110,6 +110,7 @@ func (c *Constructor) LoadWorld(ctx context.Context, from, to timeseries.Time, s prof.stage("load_jvm", func() { c.loadJVM(metrics, containers) }) prof.stage("load_dotnet", func() { c.loadDotNet(metrics, containers) }) prof.stage("load_python", func() { c.loadPython(metrics, containers) }) + prof.stage("load_nodejs", func() { c.loadNodejs(metrics, containers) }) prof.stage("enrich_instances", func() { enrichInstances(w, metrics, rdsInstancesById, ecInstancesById) }) prof.stage("calc_app_categories", func() { c.calcApplicationCategories(w) }) prof.stage("group_custom_applications", func() { c.groupCustomApplications(w) }) diff --git a/constructor/nodejs.go b/constructor/nodejs.go new file mode 100644 index 000000000..9c99d1723 --- /dev/null +++ b/constructor/nodejs.go @@ -0,0 +1,24 @@ +package constructor + +import ( + "github.com/coroot/coroot/model" + "github.com/coroot/coroot/timeseries" +) + +func (c *Constructor) loadNodejs(metrics map[string][]*model.MetricValues, containers containerCache) { + load := func(queryName string, f func(nodejs *model.Nodejs, metric *model.MetricValues)) { + for _, metric := range metrics[queryName] { + v := containers[metric.NodeContainerId] + if v.instance == nil { + continue + } + if v.instance.Nodejs == nil { + v.instance.Nodejs = &model.Nodejs{} + } + f(v.instance.Nodejs, metric) + } + } + load("container_nodejs_event_loop_blocked_time_seconds", func(nodejs *model.Nodejs, metric *model.MetricValues) { + nodejs.EventLoopBlockedTime = merge(nodejs.EventLoopBlockedTime, metric.Values, timeseries.Any) + }) +} diff --git a/constructor/queries.go b/constructor/queries.go index 36852be94..af20a3402 100644 --- a/constructor/queries.go +++ b/constructor/queries.go @@ -371,6 +371,7 @@ var QUERIES = []Query{ qDotNet("container_dotnet_thread_pool_size", `container_dotnet_thread_pool_size`), Q("container_python_thread_lock_wait_time_seconds", `rate(container_python_thread_lock_wait_time_seconds[$RANGE])`), + Q("container_nodejs_event_loop_blocked_time_seconds", `rate(container_nodejs_event_loop_blocked_time_seconds_total[$RANGE])`), } var RecordingRules = map[string]func(db *db.DB, p *db.Project, w *model.World) []*model.MetricValues{ diff --git a/model/application.go b/model/application.go index 8d17dd448..29ec8ac84 100644 --- a/model/application.go +++ b/model/application.go @@ -223,6 +223,15 @@ func (app *Application) IsPython() bool { 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 { diff --git a/model/application_types.go b/model/application_types.go index 45b989795..ba39c49f2 100644 --- a/model/application_types.go +++ b/model/application_types.go @@ -118,6 +118,8 @@ func (at ApplicationType) AuditReport() AuditReportName { return AuditReportDotNet case ApplicationTypePython: return AuditReportPython + case ApplicationTypeNodeJS: + return AuditReportNodejs } return "" } diff --git a/model/audit_report.go b/model/audit_report.go index e96066982..bbc39b5eb 100644 --- a/model/audit_report.go +++ b/model/audit_report.go @@ -28,6 +28,7 @@ const ( AuditReportJvm AuditReportName = "JVM" AuditReportDotNet AuditReportName = ".NET" AuditReportPython AuditReportName = "Python" + AuditReportNodejs AuditReportName = "Node.js" AuditReportNode AuditReportName = "Node" AuditReportDeployments AuditReportName = "Deployments" AuditReportProfiling AuditReportName = "Profiling" diff --git a/model/check.go b/model/check.go index 72164be5e..b1c45dc74 100644 --- a/model/check.go +++ b/model/check.go @@ -62,41 +62,42 @@ type CheckConfig struct { var Checks = struct { index map[CheckId]*CheckConfig - SLOAvailability CheckConfig - SLOLatency CheckConfig - CPUNode CheckConfig - CPUContainer CheckConfig - MemoryOOM CheckConfig - MemoryLeakPercent CheckConfig - StorageSpace CheckConfig - StorageIOLoad CheckConfig - NetworkRTT CheckConfig - NetworkConnectivity CheckConfig - NetworkTCPConnections CheckConfig - InstanceAvailability CheckConfig - DeploymentStatus CheckConfig - InstanceRestarts CheckConfig - RedisAvailability CheckConfig - RedisLatency CheckConfig - MongodbAvailability CheckConfig - MongodbReplicationLag CheckConfig - MemcachedAvailability CheckConfig - PostgresAvailability CheckConfig - PostgresLatency CheckConfig - PostgresReplicationLag CheckConfig - PostgresConnections CheckConfig - LogErrors CheckConfig - JvmAvailability CheckConfig - JvmSafepointTime CheckConfig - DotNetAvailability CheckConfig - PythonGILWaitingTime CheckConfig - DnsLatency CheckConfig - DnsServerErrors CheckConfig - DnsNxdomainErrors CheckConfig - MysqlAvailability CheckConfig - MysqlReplicationStatus CheckConfig - MysqlReplicationLag CheckConfig - MysqlConnections CheckConfig + SLOAvailability CheckConfig + SLOLatency CheckConfig + CPUNode CheckConfig + CPUContainer CheckConfig + MemoryOOM CheckConfig + MemoryLeakPercent CheckConfig + StorageSpace CheckConfig + StorageIOLoad CheckConfig + NetworkRTT CheckConfig + NetworkConnectivity CheckConfig + NetworkTCPConnections CheckConfig + InstanceAvailability CheckConfig + DeploymentStatus CheckConfig + InstanceRestarts CheckConfig + RedisAvailability CheckConfig + RedisLatency CheckConfig + MongodbAvailability CheckConfig + MongodbReplicationLag CheckConfig + MemcachedAvailability CheckConfig + PostgresAvailability CheckConfig + PostgresLatency CheckConfig + PostgresReplicationLag CheckConfig + PostgresConnections CheckConfig + LogErrors CheckConfig + JvmAvailability CheckConfig + JvmSafepointTime CheckConfig + DotNetAvailability CheckConfig + PythonGILWaitingTime CheckConfig + NodejsEventLoopBlockedTime CheckConfig + DnsLatency CheckConfig + DnsServerErrors CheckConfig + DnsNxdomainErrors CheckConfig + MysqlAvailability CheckConfig + MysqlReplicationStatus CheckConfig + MysqlReplicationLag CheckConfig + MysqlConnections CheckConfig }{ index: map[CheckId]*CheckConfig{}, @@ -312,6 +313,14 @@ var Checks = struct { ConditionFormatTemplate: "the time Python threads have been waiting for acquiring the GIL (Global Interpreter Lock) > ", Unit: CheckUnitSecond, }, + NodejsEventLoopBlockedTime: CheckConfig{ + Type: CheckTypeItemBased, + Title: "Node.js event loop blocked time", + DefaultThreshold: 0.7, + MessageTemplate: `high Node.js event loop blocked times on {{.Items "Node.js instance"}}`, + ConditionFormatTemplate: "the time Node.js event loop executes blocking code > ", + Unit: CheckUnitSecond, + }, DnsLatency: CheckConfig{ Type: CheckTypeValueBased, Title: "DNS latency", diff --git a/model/instance.go b/model/instance.go index ceff4eba5..695537259 100644 --- a/model/instance.go +++ b/model/instance.go @@ -39,6 +39,7 @@ type Instance struct { Jvms map[string]*Jvm DotNet map[string]*DotNet Python *Python + Nodejs *Nodejs Volumes []*Volume diff --git a/model/nodejs.go b/model/nodejs.go new file mode 100644 index 000000000..188953231 --- /dev/null +++ b/model/nodejs.go @@ -0,0 +1,7 @@ +package model + +import "github.com/coroot/coroot/timeseries" + +type Nodejs struct { + EventLoopBlockedTime *timeseries.TimeSeries +}