otel.go (6894B)
1 package otelsql 2 3 import ( 4 "context" 5 "database/sql" 6 "database/sql/driver" 7 "io" 8 "time" 9 10 "go.opentelemetry.io/otel" 11 "go.opentelemetry.io/otel/attribute" 12 "go.opentelemetry.io/otel/codes" 13 "go.opentelemetry.io/otel/metric" 14 semconv "go.opentelemetry.io/otel/semconv/v1.10.0" 15 "go.opentelemetry.io/otel/trace" 16 ) 17 18 const instrumName = "github.com/uptrace/opentelemetry-go-extra/otelsql" 19 20 var dbRowsAffected = attribute.Key("db.rows_affected") 21 22 type config struct { 23 tracerProvider trace.TracerProvider 24 tracer trace.Tracer //nolint:structcheck 25 26 meterProvider metric.MeterProvider 27 meter metric.Meter 28 29 attrs []attribute.KeyValue 30 31 queryFormatter func(query string) string 32 } 33 34 func newConfig(opts []Option) *config { 35 c := &config{ 36 tracerProvider: otel.GetTracerProvider(), 37 meterProvider: otel.GetMeterProvider(), 38 } 39 for _, opt := range opts { 40 opt(c) 41 } 42 return c 43 } 44 45 func (c *config) formatQuery(query string) string { 46 if c.queryFormatter != nil { 47 return c.queryFormatter(query) 48 } 49 return query 50 } 51 52 type dbInstrum struct { 53 *config 54 55 queryHistogram metric.Int64Histogram 56 } 57 58 func newDBInstrum(opts []Option) *dbInstrum { 59 t := &dbInstrum{ 60 config: newConfig(opts), 61 } 62 63 if t.tracer == nil { 64 t.tracer = t.tracerProvider.Tracer(instrumName) 65 } 66 if t.meter == nil { 67 t.meter = t.meterProvider.Meter(instrumName) 68 } 69 70 var err error 71 t.queryHistogram, err = t.meter.Int64Histogram( 72 "go.sql.query_timing", 73 metric.WithDescription("Timing of processed queries"), 74 metric.WithUnit("milliseconds"), 75 ) 76 if err != nil { 77 panic(err) 78 } 79 80 return t 81 } 82 83 func (t *dbInstrum) withSpan( 84 ctx context.Context, 85 spanName string, 86 query string, 87 fn func(ctx context.Context, span trace.Span) error, 88 ) error { 89 var startTime time.Time 90 if query != "" { 91 startTime = time.Now() 92 } 93 94 attrs := make([]attribute.KeyValue, 0, len(t.attrs)+1) 95 attrs = append(attrs, t.attrs...) 96 if query != "" { 97 attrs = append(attrs, semconv.DBStatementKey.String(t.formatQuery(query))) 98 } 99 100 ctx, span := t.tracer.Start(ctx, spanName, 101 trace.WithSpanKind(trace.SpanKindClient), 102 trace.WithAttributes(attrs...)) 103 err := fn(ctx, span) 104 span.End() 105 106 if query != "" { 107 t.queryHistogram.Record(ctx, time.Since(startTime).Milliseconds(), metric.WithAttributes(t.attrs...)) 108 } 109 110 if !span.IsRecording() { 111 return err 112 } 113 114 switch err { 115 case nil, 116 driver.ErrSkip, 117 io.EOF, // end of rows iterator 118 sql.ErrNoRows: 119 // ignore 120 default: 121 span.RecordError(err) 122 span.SetStatus(codes.Error, err.Error()) 123 } 124 125 return err 126 } 127 128 type Option func(c *config) 129 130 // WithTracerProvider configures a tracer provider that is used to create a tracer. 131 func WithTracerProvider(tracerProvider trace.TracerProvider) Option { 132 return func(c *config) { 133 c.tracerProvider = tracerProvider 134 } 135 } 136 137 // WithAttributes configures attributes that are used to create a span. 138 func WithAttributes(attrs ...attribute.KeyValue) Option { 139 return func(c *config) { 140 c.attrs = append(c.attrs, attrs...) 141 } 142 } 143 144 // WithDBSystem configures a db.system attribute. You should prefer using 145 // WithAttributes and semconv, for example, `otelsql.WithAttributes(semconv.DBSystemSqlite)`. 146 func WithDBSystem(system string) Option { 147 return func(c *config) { 148 c.attrs = append(c.attrs, semconv.DBSystemKey.String(system)) 149 } 150 } 151 152 // WithDBName configures a db.name attribute. 153 func WithDBName(name string) Option { 154 return func(c *config) { 155 c.attrs = append(c.attrs, semconv.DBNameKey.String(name)) 156 } 157 } 158 159 // WithMeterProvider configures a metric.Meter used to create instruments. 160 func WithMeterProvider(meterProvider metric.MeterProvider) Option { 161 return func(c *config) { 162 c.meterProvider = meterProvider 163 } 164 } 165 166 // WithQueryFormatter configures a query formatter 167 func WithQueryFormatter(queryFormatter func(query string) string) Option { 168 return func(c *config) { 169 c.queryFormatter = queryFormatter 170 } 171 } 172 173 // ReportDBStatsMetrics reports DBStats metrics using OpenTelemetry Metrics API. 174 func ReportDBStatsMetrics(db *sql.DB, opts ...Option) { 175 cfg := newConfig(opts) 176 177 if cfg.meter == nil { 178 cfg.meter = cfg.meterProvider.Meter(instrumName) 179 } 180 181 meter := cfg.meter 182 labels := cfg.attrs 183 184 maxOpenConns, _ := meter.Int64ObservableGauge( 185 "go.sql.connections_max_open", 186 metric.WithDescription("Maximum number of open connections to the database"), 187 ) 188 openConns, _ := meter.Int64ObservableGauge( 189 "go.sql.connections_open", 190 metric.WithDescription("The number of established connections both in use and idle"), 191 ) 192 inUseConns, _ := meter.Int64ObservableGauge( 193 "go.sql.connections_in_use", 194 metric.WithDescription("The number of connections currently in use"), 195 ) 196 idleConns, _ := meter.Int64ObservableGauge( 197 "go.sql.connections_idle", 198 metric.WithDescription("The number of idle connections"), 199 ) 200 connsWaitCount, _ := meter.Int64ObservableCounter( 201 "go.sql.connections_wait_count", 202 metric.WithDescription("The total number of connections waited for"), 203 ) 204 connsWaitDuration, _ := meter.Int64ObservableCounter( 205 "go.sql.connections_wait_duration", 206 metric.WithDescription("The total time blocked waiting for a new connection"), 207 metric.WithUnit("nanoseconds"), 208 ) 209 connsClosedMaxIdle, _ := meter.Int64ObservableCounter( 210 "go.sql.connections_closed_max_idle", 211 metric.WithDescription("The total number of connections closed due to SetMaxIdleConns"), 212 ) 213 connsClosedMaxIdleTime, _ := meter.Int64ObservableCounter( 214 "go.sql.connections_closed_max_idle_time", 215 metric.WithDescription("The total number of connections closed due to SetConnMaxIdleTime"), 216 ) 217 connsClosedMaxLifetime, _ := meter.Int64ObservableCounter( 218 "go.sql.connections_closed_max_lifetime", 219 metric.WithDescription("The total number of connections closed due to SetConnMaxLifetime"), 220 ) 221 222 if _, err := meter.RegisterCallback( 223 func(ctx context.Context, o metric.Observer) error { 224 stats := db.Stats() 225 226 o.ObserveInt64(maxOpenConns, int64(stats.MaxOpenConnections), metric.WithAttributes(labels...)) 227 228 o.ObserveInt64(openConns, int64(stats.OpenConnections), metric.WithAttributes(labels...)) 229 o.ObserveInt64(inUseConns, int64(stats.InUse), metric.WithAttributes(labels...)) 230 o.ObserveInt64(idleConns, int64(stats.Idle), metric.WithAttributes(labels...)) 231 232 o.ObserveInt64(connsWaitCount, stats.WaitCount, metric.WithAttributes(labels...)) 233 o.ObserveInt64(connsWaitDuration, int64(stats.WaitDuration), metric.WithAttributes(labels...)) 234 o.ObserveInt64(connsClosedMaxIdle, stats.MaxIdleClosed, metric.WithAttributes(labels...)) 235 o.ObserveInt64(connsClosedMaxIdleTime, stats.MaxIdleTimeClosed, metric.WithAttributes(labels...)) 236 o.ObserveInt64(connsClosedMaxLifetime, stats.MaxLifetimeClosed, metric.WithAttributes(labels...)) 237 238 return nil 239 }, 240 maxOpenConns, 241 openConns, 242 inUseConns, 243 idleConns, 244 connsWaitCount, 245 connsWaitDuration, 246 connsClosedMaxIdle, 247 connsClosedMaxIdleTime, 248 connsClosedMaxLifetime, 249 ); err != nil { 250 panic(err) 251 } 252 }