Documentation
¶
Overview ¶
Package observability provides hooks for exposing query timing and error metrics. Currently uses a simple in-memory collector interface that can be wired to Prometheus, OpenTelemetry, or any metrics backend.
Index ¶
- Constants
- Variables
- func ConnectionFromURL(dsn string) (namespace, address string, port int)
- func MetricsWithCollectionLabels(m gometrics.Metrics, c CollectionLabels) gometrics.Metrics
- func StatusCodeInt(code int64) string
- type CollectionLabels
- type Collector
- type Hook
- func (h *Hook) AfterMutation(_ context.Context, qc *hook.QueryContext, _, _ any) error
- func (h *Hook) AfterQuery(_ context.Context, qc *hook.QueryContext, _ any) error
- func (h *Hook) BeforeMutation(_ context.Context, qc *hook.QueryContext, _ any) (*hook.HookResult, error)
- func (h *Hook) BeforeQuery(_ context.Context, qc *hook.QueryContext) (*hook.HookResult, error)
- type InMemoryCollector
- type MetricEntry
- type Op
- type Option
- type SQLSummary
- type Telemetry
- func (t *Telemetry) Namespace() string
- func (t *Telemetry) SetConnection(namespace, address string, port int)
- func (t *Telemetry) SetMetrics(m gometrics.Metrics)
- func (t *Telemetry) Start(ctx context.Context, operation, collection string) (context.Context, *Op)
- func (t *Telemetry) StartIn(ctx context.Context, namespace, operation, collection string) (context.Context, *Op)
- func (t *Telemetry) StartSQL(ctx context.Context, query string) (context.Context, *Op)
Constants ¶
const DefaultCollectionLabelLimit = 256
DefaultCollectionLabelLimit is how many distinct db.collection.name values one Telemetry puts on OperationDurationMetric before it records every new one as OtherCollection.
const InstrumentationName = "github.com/xraph/grove"
InstrumentationName is the tracer name on every span grove starts.
const OperationDurationMetric = "db.client.operation.duration"
OperationDurationMetric is the OTel database client histogram every driver records. A Forge app scrapes it as db_client_operation_duration_seconds.
const OtherCollection = "other"
OtherCollection is the db.collection.name metric label a collection gets once the limit is reached. A label function can return it too.
Variables ¶
var OperationDurationBuckets = []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10}
OperationDurationBuckets are the bounds the OTel database conventions advise for OperationDurationMetric, in seconds.
Functions ¶
func ConnectionFromURL ¶ added in v1.8.0
ConnectionFromURL reads the database name, host and port out of a URL-form DSN (clickhouse://host:9000/db, libsql://name.turso.io) for SetConnection. The database name comes from the path, or from a database query parameter when the path is empty. Credentials are never read. A DSN that isn't a URL with a host, such as a local file, reports nothing.
func MetricsWithCollectionLabels ¶ added in v1.9.0
func MetricsWithCollectionLabels(m gometrics.Metrics, c CollectionLabels) gometrics.Metrics
MetricsWithCollectionLabels returns m carrying c. Telemetry.SetMetrics records on m and applies c, so whoever hands a driver its metrics sink can set the collection label without the driver knowing about it. grove.Open does this for grove.WithCollectionLabel and grove.WithCollectionLabelLimit. If m already carries settings, a field c leaves unset (a nil Label, a Limit of zero or less) keeps the one m had. A nil m returns nil.
func StatusCodeInt ¶ added in v1.8.0
StatusCodeInt formats an integer error code for WithStatusCode functions.
Types ¶
type CollectionLabels ¶ added in v1.9.0
type CollectionLabels struct {
// Label maps a collection to its label value, in place of the default.
// It gets the collection as the driver reported it, never empty: for SQL
// the first table as written, schema included (tenant_42.orders), for
// MongoDB and Elasticsearch the collection or index. Return "" to record
// no collection or OtherCollection to fold it in with the rest. Any other
// value counts toward Limit. It runs on every recorded call, so keep it
// cheap, and it must be safe for concurrent use.
Label func(collection string) string
// Limit caps the distinct label values one Telemetry records. Zero or
// less means DefaultCollectionLabelLimit.
Limit int
}
CollectionLabels decides the db.collection.name label on OperationDurationMetric. Spans don't use it: they carry the collection as the driver reported it.
The zero value is the default. A SQL table loses its schema or database qualifier, so tenant_42.orders and tenant_43.orders both record as orders. A MongoDB collection or Elasticsearch index is used as reported, since a dot is part of its name there and the database is already db.namespace. After DefaultCollectionLabelLimit distinct values, a new one records as OtherCollection; the values already seen keep recording under their own.
type Collector ¶
type Collector interface {
// RecordQuery records timing for a query/mutation.
RecordQuery(table, operation string, duration time.Duration, err error)
}
Collector is the interface for recording metrics.
type Hook ¶
type Hook struct {
// contains filtered or unexported fields
}
Hook is a combined PreQueryHook and PostQueryHook that measures query timing.
func (*Hook) AfterMutation ¶
AfterMutation records the duration for mutations.
func (*Hook) AfterQuery ¶
AfterQuery records the duration.
func (*Hook) BeforeMutation ¶
func (h *Hook) BeforeMutation(_ context.Context, qc *hook.QueryContext, _ any) (*hook.HookResult, error)
BeforeMutation records the start time for mutations.
func (*Hook) BeforeQuery ¶
func (h *Hook) BeforeQuery(_ context.Context, qc *hook.QueryContext) (*hook.HookResult, error)
BeforeQuery records the start time.
type InMemoryCollector ¶
type InMemoryCollector struct {
TotalQueries atomic.Int64
TotalErrors atomic.Int64
// contains filtered or unexported fields
}
InMemoryCollector is a simple in-memory metrics collector for testing.
func NewInMemoryCollector ¶
func NewInMemoryCollector() *InMemoryCollector
NewInMemoryCollector creates a new in-memory collector.
func (*InMemoryCollector) Entries ¶
func (c *InMemoryCollector) Entries() []MetricEntry
Entries returns all recorded metric entries.
func (*InMemoryCollector) RecordQuery ¶
func (c *InMemoryCollector) RecordQuery(table, operation string, duration time.Duration, err error)
func (*InMemoryCollector) Reset ¶
func (c *InMemoryCollector) Reset()
Reset clears all recorded entries.
type MetricEntry ¶
type MetricEntry struct {
Table string
Operation string
Duration time.Duration
Error error
Timestamp time.Time
}
MetricEntry represents a single recorded metric.
type Op ¶ added in v1.8.0
type Op struct {
// contains filtered or unexported fields
}
Op is one call to the database, from Start to End.
func (*Op) Context ¶ added in v1.8.0
Context returns ctx with the operation's span on it, for code that has to hand a context to the driver after Start returned. A nil *Op returns ctx.
func (*Op) End ¶ added in v1.8.0
End ends the span and records the duration. A nil err, or one the driver marked benign, is a success. Anything else sets error.type, db.response.status_code when the driver can read one, an exception event carrying the error's Go type, and an error status on the span. The error's text never goes on the span, because a driver's message can quote the values it was given (a duplicate key, a bad integer). End is safe on a nil *Op and does nothing the second time.
func (*Op) SetBatchSize ¶ added in v1.8.0
SetBatchSize records db.operation.batch.size for a call that carries more than one operation, like a bulk write. Sizes under 2 are ignored, as the conventions ask.
func (*Op) SetCollection ¶ added in v1.8.0
SetCollection names the collection after Start, for a client that reports it late, like the Elasticsearch client reporting the index once the request path is built. It renames the span and sets the metric label to match. The name is taken as is, with no SQL qualifier to drop.
type Option ¶ added in v1.8.0
type Option func(*Telemetry)
Option configures a Telemetry.
func WithBenign ¶ added in v1.8.0
WithBenign marks errors that are normal outcomes rather than failures, such as a document not found. They end the span without an error status and record no error.type. sql.ErrNoRows is always benign.
func WithStatusCode ¶ added in v1.8.0
WithStatusCode sets how the driver reads db.response.status_code out of an error: a Postgres SQLSTATE, a MySQL error number, a MongoDB error code. The function returns "" when the error carries none.
type SQLSummary ¶ added in v1.8.0
type SQLSummary struct {
// Operation is the db.operation.name: an upper-case keyword such as
// SELECT, INSERT, COMMIT or CREATE TABLE. Empty when the statement starts
// with something we don't recognise.
Operation string
// Collection is the db.collection.name: the first table the statement
// reads or writes, as written (schema prefix kept, quotes dropped). Empty
// when there isn't one, or when it's a subquery.
Collection string
// Summary is Operation and Collection joined by a space, or Operation
// alone. Drivers use it as the span name.
Summary string
}
SQLSummary is what SummarizeSQL reads out of a statement: the operation, the first table it works on, and the two joined as the OTel db.query.summary. None of the three ever holds a value from the statement.
func SummarizeSQL ¶ added in v1.8.0
func SummarizeSQL(query string) SQLSummary
SummarizeSQL reads the operation and the first table out of a SQL statement. It reads the statement's shape, never its values, so the result is safe to put on a span or a metric label: SELECT, INSERT INTO, UPDATE, DELETE FROM, MERGE INTO, the CTE behind a WITH, and DDL on tables.
It is a scanner, not a parser. Comments and quoted strings are skipped, and a table inside parentheses (a subquery) is never reported.
type Telemetry ¶ added in v1.8.0
type Telemetry struct {
// contains filtered or unexported fields
}
Telemetry traces and times the calls one driver makes, following the OpenTelemetry database client conventions. Each driver builds one in New and calls Start (or StartSQL) around every call that reaches the database.
Spans go to the global tracer provider, which forge.New installs. A span is only started when the context already carries one, so a background poll that finds nothing doesn't produce a root trace per tick: give the unit of work its own span and the queries under it nest inside. The duration histogram is recorded either way, once SetMetrics has been called.
A nil *Telemetry is valid and does nothing.
func NewTelemetry ¶ added in v1.8.0
NewTelemetry returns the telemetry for one driver instance. system is the db.system.name value: postgresql, mysql, sqlite, mongodb, clickhouse, elasticsearch and so on.
func (*Telemetry) Namespace ¶ added in v1.8.0
Namespace returns the db.namespace set by SetConnection.
func (*Telemetry) SetConnection ¶ added in v1.8.0
SetConnection records where the driver is connected: the database name (db.namespace) and the server's address and port. Call it from Open, before the first query. Empty values and a zero port are left off.
func (*Telemetry) SetMetrics ¶ added in v1.8.0
SetMetrics starts recording OperationDurationMetric on m. In a Forge app, pass app.Metrics(); grove's Forge extension does this for you. A nil m stops recording.
The db.collection.name label follows the CollectionLabels attached by MetricsWithCollectionLabels, or the default when m carries none. Each call starts a fresh count toward the label limit.
func (*Telemetry) Start ¶ added in v1.8.0
Start starts an operation the driver describes itself, for databases that aren't SQL: operation is the command (find, insert, search) and collection the collection or index, "" when there isn't one.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package telemetrytest records the spans and metrics grove's drivers emit, so a driver test can assert on them without a Forge app.
|
Package telemetrytest records the spans and metrics grove's drivers emit, so a driver test can assert on them without a Forge app. |