observability

package
v1.10.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
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.

View Source
const InstrumentationName = "github.com/xraph/grove"

InstrumentationName is the tracer name on every span grove starts.

View Source
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.

View Source
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

View Source
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

func ConnectionFromURL(dsn string) (namespace, address string, port int)

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

func StatusCodeInt(code int64) string

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 NewHook

func NewHook(c Collector) *Hook

NewHook creates a new observability hook with the given collector.

func (*Hook) AfterMutation

func (h *Hook) AfterMutation(_ context.Context, qc *hook.QueryContext, _, _ any) error

AfterMutation records the duration for mutations.

func (*Hook) AfterQuery

func (h *Hook) AfterQuery(_ context.Context, qc *hook.QueryContext, _ any) error

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

func (o *Op) Context(ctx context.Context) context.Context

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

func (o *Op) End(err error)

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

func (o *Op) SetBatchSize(n int)

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

func (o *Op) SetCollection(collection string)

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

func WithBenign(fn func(error) bool) Option

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

func WithStatusCode(fn func(error) string) Option

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

func NewTelemetry(system string, opts ...Option) *Telemetry

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

func (t *Telemetry) Namespace() string

Namespace returns the db.namespace set by SetConnection.

func (*Telemetry) SetConnection added in v1.8.0

func (t *Telemetry) SetConnection(namespace, address string, port int)

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

func (t *Telemetry) SetMetrics(m gometrics.Metrics)

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

func (t *Telemetry) Start(ctx context.Context, operation, collection string) (context.Context, *Op)

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.

func (*Telemetry) StartIn added in v1.8.0

func (t *Telemetry) StartIn(ctx context.Context, namespace, operation, collection string) (context.Context, *Op)

StartIn is Start for a driver whose namespace changes per call, like a MongoDB client that talks to several databases.

func (*Telemetry) StartSQL added in v1.8.0

func (t *Telemetry) StartSQL(ctx context.Context, query string) (context.Context, *Op)

StartSQL starts an operation for a SQL statement. The operation, table and span name come from SummarizeSQL, so they never hold a value from query.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL