Added OTEL DB stats metrics, renamed trace attributes to expected OTEL names (#42097)

1. Added DB metrics via otelsql.RegisterDBStatsMetrics()
`db.sql.connection.open`
`db.sql.connection.max_open`
`db.sql.connection.wait`
`db.sql.connection.wait_duration`
`db.sql.connection.closed_max_idle`
`db.sql.connection.closed_max_idle_time`
`db.sql.latency.*`
2. renamed these metrics to signoz convention/expected names
`db.sql.connection.open` -> `db.client.connection.usage`
`db.sql.connection.max_open` -> `db.client.connection.max`
`db.sql.connection.wait` -> `db.client.connection.wait_count`
`db.sql.connection.wait_duration` -> `db.client.connection.wait_time`
`db.sql.connection.closed_max_idle` -> `db.client.connection.idle.max`
`db.sql.connection.closed_max_idle_time` ->
`db.client.connection.idle.min`
3. created custom dashboard to display these metrics, (import via json)
<img width="1580" height="906" alt="Screenshot 2026-03-19 at 2 44 43 PM"
src="https://github.com/user-attachments/assets/f1b64ed6-e534-4490-8955-bc1205dd21d4"
/>
4. Fixed metrics for service db dashboards
Signoz expects

`db.system` : Identifies the database type (e.g., postgresql, mysql,
mongodb).
`db.statement` : The actual query being executed (e.g., SELECT * FROM
users).
`db.operation` : The type of operation (e.g., SELECT, INSERT).
`service.name` : The name of the service making the call.

We needed to set the `db.system` attribute explicitly.

`db.operation` is missing because otelsql doesn't capture this by
default. Decided not to add this for now as the dashboards work without.
Can be a future enhancement.

<img width="1563" height="487" alt="Screenshot 2026-03-19 at 2 45 18 PM"
src="https://github.com/user-attachments/assets/51028e16-ee2c-45a9-9025-26f17b0db67a"
/>


# Checklist for submitter

## Testing
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added a new observability dashboard for database and connection
performance metrics, including RPS, latency, connection pool saturation,
and queue statistics.
* Enhanced database metrics collection with automatic registration of
connection and query performance indicators.
* Standardized OpenTelemetry metric naming to align with industry
conventions for improved observability compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Konstantin Sykulev 2026-03-20 12:07:58 -04:00 committed by GitHub
parent 40e91c0ece
commit 6ed3ba6801
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1800 additions and 1 deletions

View file

@ -228,9 +228,40 @@ func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, dev
if err != nil {
initFatal(err, "Failed to initialize OTEL metrics exporter")
}
// Create views to rename otelsql metrics to match what OpenTelemetry Signoz expects
// Reference: https://opentelemetry.io/docs/specs/semconv/db/database-metrics/
dbMetricViews := []sdkmetric.View{
sdkmetric.NewView(
sdkmetric.Instrument{Name: "db.sql.connection.open"},
sdkmetric.Stream{Name: "db.client.connection.count"},
),
sdkmetric.NewView(
sdkmetric.Instrument{Name: "db.sql.connection.max_open"},
sdkmetric.Stream{Name: "db.client.connection.max"},
),
sdkmetric.NewView(
sdkmetric.Instrument{Name: "db.sql.connection.wait"},
sdkmetric.Stream{Name: "db.client.connection.wait_count"},
),
sdkmetric.NewView(
sdkmetric.Instrument{Name: "db.sql.connection.wait_duration"},
sdkmetric.Stream{Name: "db.client.connection.wait_time"},
),
sdkmetric.NewView(
sdkmetric.Instrument{Name: "db.sql.connection.closed_max_idle"},
sdkmetric.Stream{Name: "db.client.connection.closed.max_idle"},
),
sdkmetric.NewView(
sdkmetric.Instrument{Name: "db.sql.connection.closed_max_idle_time"},
sdkmetric.Stream{Name: "db.client.connection.closed.max_idle_time"},
),
}
meterProvider = sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)),
sdkmetric.WithView(dbMetricViews...),
)
otel.SetMeterProvider(meterProvider)

View file

@ -35,6 +35,7 @@ import (
"github.com/go-sql-driver/mysql"
"github.com/hashicorp/go-multierror"
"github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
)
@ -342,7 +343,10 @@ var otelTracedDriverName string
func init() {
var err error
otelTracedDriverName, err = otelsql.Register("mysql",
otelsql.WithAttributes(semconv.DBSystemNameMySQL),
otelsql.WithAttributes(
attribute.String("db.system", "mysql"),
semconv.DBSystemNameMySQL,
),
otelsql.WithSpanOptions(otelsql.SpanOptions{
// DisableErrSkip ignores driver.ErrSkip errors which are frequently returned by the MySQL driver
// when certain optional methods or paths are not implemented/taken.

View file

@ -9,10 +9,12 @@ import (
"net/url"
"time"
"github.com/XSAM/otelsql"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/ngrok/sqlmw"
"go.opentelemetry.io/otel/attribute"
)
// ConnectorFactory creates a driver.Connector for custom database authentication.
@ -114,6 +116,26 @@ func NewDB(conf *MysqlConfig, opts *DBOptions, otelDriverName string) (*sqlx.DB,
if dbError != nil {
return nil, dbError
}
// Register database/sql.DBStats metrics when using OpenTelemetry tracing.
if opts.TracingConfig != nil && opts.TracingConfig.TracingEnabled && opts.TracingConfig.TracingType != "elasticapm" {
attrs := []attribute.KeyValue{
attribute.String("db.system", "mysql"),
}
// Parse DSN to extract address and database name for metric differentiation
if cfg, err := mysql.ParseDSN(dsn); err == nil {
if cfg.Addr != "" {
attrs = append(attrs, attribute.String("db.addr", cfg.Addr))
}
if cfg.DBName != "" {
attrs = append(attrs, attribute.String("db.name", cfg.DBName))
}
}
if err := otelsql.RegisterDBStatsMetrics(db.DB, otelsql.WithAttributes(attrs...)); err != nil {
opts.Logger.WarnContext(context.Background(), "failed to register DB stats metrics", "err", err)
}
}
return db, nil
}

File diff suppressed because it is too large Load diff