observability

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package observability holds the adapters for logging, metrics and tracing.

This file is the logger. Structured JSON in operation, text for a developer's terminal, and a redaction layer that no caller has to remember (threat T-18): the log is written by many hands, including hands that pass an error along without reading it, and an error is where a connection string travels.

Index

Constants

View Source
const Redacted = "[REDACTED]"

Redacted is what replaces a value that must not be readable. A fixed marker rather than an empty string, so that the log still shows the field was present.

View Source
const ResultOK = "ok"

ResultOK is the `result` value of a use case that succeeded. Every other value is an error category of the domain error model, lower-cased - see ResultClass.

Variables

This section is empty.

Functions

func NewCorrelatingHandler

func NewCorrelatingHandler(inner slog.Handler) slog.Handler

NewCorrelatingHandler adds trace_id, span_id, request_id, tenant_id and component from the context to every record.

It is a handler rather than a rule every call site has to remember, because the call sites are many and the one that forgets is the one being debugged. The fields are mandatory in observability-reliability.md §3.1 for exactly that reason: without them a log line and a trace are two unrelated pieces of evidence.

It belongs outside the redacting handler, so the attributes it adds pass the redaction like any other - a handler that writes straight to the sink would be a hole in T-18.

func NewLogger

func NewLogger(cfg env.Config, w io.Writer) *slog.Logger

NewLogger builds the process logger from the configuration.

The handler chain is correlation → redaction → sink. Correlation goes outermost so that the trace_id and request_id it adds pass the redaction like any other attribute; a handler writing straight to the sink would be a hole in T-18.

func NewRedactingHandler

func NewRedactingHandler(inner slog.Handler) slog.Handler

NewRedactingHandler wraps a handler so that every attribute passes the redaction on its way through - including the attributes of With and WithGroup, which are applied once and then reused for the lifetime of a logger.

func ResultClass

func ResultClass(err error) string

ResultClass is the `result` label of hubtask_usecase_total: ok, or the error category in lower case (observability-reliability.md §4.1).

It derives rather than translates, and that is the whole design. A mapping table would be a second classification of the same failure, and the two would disagree the moment the domain grows a tenth category - which would then be folded into `internal` by a `default` branch and vanish. Deriving means a new category appears in the metrics by itself.

The coarse view an alert wants is a query, not a label: `result=~"internal|unavailable"`. Folding rate_limited into internal here would save that regular expression and cost the distinction between a defect and a limit doing its job - which is the one an alert must not get wrong (§1, "alert on symptoms").

AsError normalises anything unclassified to INTERNAL, so the result is total: every error produces exactly one of the values the domain defines.

Types

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics owns the meter and the instruments that exist before any use case does. Handing out an instrument rather than the meter is deliberate: an instrument created at a call site is created per call, and the label set of one created ad hoc is nobody's decision.

func NewMetrics

func NewMetrics(cfg env.Config) (*Metrics, error)

NewMetrics builds the meter and registers what the process needs from the first second: the panic counter above all, whose value is the one an alert watches (ADR-0016).

func (*Metrics) AiCall

func (m *Metrics) AiCall(ctx context.Context, kind, operation, result string)

AiCall counts one finished AI provider call. Three labels, all closed sets: the provider kind, the operation, and the result as the domain's own error category in lower case. The endpoint is not among them - it is a tenant's configuration, and a label per endpoint would grow a series per customer (§3.2, rule 10).

What is deliberately absent is anything a person wrote. The one place in this system where a title reaches something outside it is the call this counts, so the counting is the place the rule is easiest to break.

func (*Metrics) AiTokens

func (m *Metrics) AiTokens(ctx context.Context, kind, operation string, input, output int)

AiTokens records what a call consumed, in the one unit every provider reports. It feeds the tenant's budget and an operator's sense of cost, and it is not a price - this project does not have one.

func (*Metrics) AuditDefaultAgedRows

func (m *Metrics) AuditDefaultAgedRows(ctx context.Context, rows int64)

AuditDefaultAgedRows is how many entries in the trail's default partition are past their period.

func (*Metrics) AuditPartitionsHeld

func (m *Metrics) AuditPartitionsHeld(ctx context.Context, months int64)

AuditPartitionsHeld is how many of the trail's aged months a hold keeps.

func (*Metrics) AuthFailure

func (m *Metrics) AuthFailure(ctx context.Context, reason string)

AuthFailure counts one refused sign-in or refresh attempt, by reason. The reasons are the closed set the identity service declares - never a subject, never an address (§3.2, rule 10).

func (*Metrics) BackupLastSuccess

func (m *Metrics) BackupLastSuccess(ctx context.Context, targetID string, at time.Time)

BackupLastSuccess publishes when a target last had a backup that worked - the number alert A-12 watches (backup-restore.md §10).

A Unix timestamp rather than an age, which is the convention every backup exporter follows and the reason it works: an age computed here is stale the moment it is scraped, while a timestamp lets the alert compute the age at evaluation time and stay right between scrapes.

Labelled by target and not by tenant. Which targets exist is bounded by configuration; how many tenants there are is not, and an unbounded label is how a metrics backend falls over (observability-reliability.md §3.2).

func (*Metrics) BusPublished

func (m *Metrics) BusPublished(ctx context.Context, eventType string)

BusPublished counts one event that reached the bus. The type is a closed set - the event types this build emits - and the subject is deliberately not a label: it carries a tenant identifier, and a series per tenant is how a Prometheus dies and how a workspace becomes visible in somebody else's dashboard (§3.2, rule 10).

func (*Metrics) BusRefused

func (m *Metrics) BusRefused(ctx context.Context, reason string)

BusRefused counts one that did not, by reason. The reason is written by hand where it is raised and stays a short closed set: `unavailable` for a bus that could not be reached, `event_expired` for an event swept before its publication ran.

func (*Metrics) CircuitBreakerState

func (m *Metrics) CircuitBreakerState(ctx context.Context, dependency string, level int64)

CircuitBreakerState publishes the state of one breaker: 0 closed, 1 half-open, 2 open. The values come from resilience.BreakerState.Level(), which owns them - the gauge only reports what it is handed, so that the dashboard's contract has a single source (§4).

The composition root passes this as the breaker's OnStateChange hook. It takes the level rather than the state type, so that the metrics adapter does not have to know the resilience adapter: adapters do not know each other (project-structure.md §2).

func (*Metrics) ConfigInvalid

func (m *Metrics) ConfigInvalid(ctx context.Context, key string)

ConfigInvalid counts a rejected configuration variable - misconfiguration as a signal rather than as guesswork in a support conversation.

func (*Metrics) DataSubjectDeadline

func (m *Metrics) DataSubjectDeadline(ctx context.Context, stage string, count int)

DataSubjectDeadline reports cases whose statutory deadline is near or past - alert A-19's number (data-protection.md §4).

A counter by stage rather than a gauge of how many are open, and that is about what is true in provider operation: a gauge would need a tenant label to be true there, and an unlabelled one would carry the last workspace's number rather than the installation's. "A deadline is approaching somewhere" is true either way, and it is what the alert asks.

func (*Metrics) DegradedMode

func (m *Metrics) DegradedMode(ctx context.Context, feature string, degraded bool)

DegradedMode reports a feature as restricted, which is what makes a partial outage visible without reading the health endpoint.

func (*Metrics) DependencyUp

func (m *Metrics) DependencyUp(ctx context.Context, dependency string, up bool)

DependencyUp mirrors the health report into a time series, so that a disruption has a beginning and an end in the same place as everything else.

func (*Metrics) HTTPRequest

func (m *Metrics) HTTPRequest(ctx context.Context, route, method string, status int, seconds float64)

HTTPRequest records one served request. route is the template (/items/{id}), never the resolved path - the resolved one carries identifiers and would be unbounded.

func (*Metrics) Handler

func (m *Metrics) Handler() http.Handler

Handler serves the Prometheus endpoint. It belongs on the ops port, never on the public one (§3.2): the series say what runs, how much of it, and how slowly.

func (*Metrics) InflightDelta

func (m *Metrics) InflightDelta(ctx context.Context, role string, delta int64)

InflightDelta moves the gauge of requests currently in flight.

func (*Metrics) JobDeadLettered

func (m *Metrics) JobDeadLettered(ctx context.Context, kind string)

JobDeadLettered counts a job that used up its attempts (alert A-07).

func (*Metrics) JobFailed

func (m *Metrics) JobFailed(ctx context.Context, kind, attemptClass string)

JobFailed records one failed attempt. attemptClass is retry or final, which is the distinction an alert needs: retries are the system working, and a final failure is a person's problem.

func (*Metrics) JobFinished

func (m *Metrics) JobFinished(ctx context.Context, kind string, seconds float64)

JobFinished records a job that ran to the end. kind is the job kind, which is a closed set in core/port/queue - never a tenant, never an identifier.

func (*Metrics) JobsPurged

func (m *Metrics) JobsPurged(ctx context.Context, state string, count int64)

JobsPurged counts the finished jobs the leader removed, by terminal state. Written at zero too, for RetentionDeleted's reason.

func (*Metrics) MediaReclaimFailed

func (m *Metrics) MediaReclaimFailed(ctx context.Context, count int64)

MediaReclaimFailed counts the orphans a pass could not reclaim. They stay marked and the next pass tries again, so a number that keeps rising is a bucket that is not letting go rather than a backlog that will clear.

func (*Metrics) MediaReclaimed

func (m *Metrics) MediaReclaimed(ctx context.Context, count int64)

MediaReclaimed counts what a pass removed. Written even when it is zero, for the reason RetentionDeleted is.

func (*Metrics) MigrationVersion

func (m *Metrics) MigrationVersion(ctx context.Context, version int64)

MigrationVersion publishes the migration this build embeds, once at startup. It deliberately reports the build, not the database: every pod asks the same database, so the database's answer could never differ between pods, and differing is the whole signal (A-13).

func (*Metrics) NotificationFailed

func (m *Metrics) NotificationFailed(ctx context.Context, category, channel, reason string)

NotificationFailed counts a send that did not work, by the detail code that says why. The reason separates "the server is down" from "the address was refused", which are different problems with different fixes.

func (*Metrics) NotificationRecorded

func (m *Metrics) NotificationRecorded(ctx context.Context, category, channel, state string)

NotificationRecorded counts a record as it is written, in the state it was written in. The state is the point: a rising SUPPRESSED count is people switching things off, and a rising PENDING count that never becomes SENT is a mail server nobody has noticed.

func (*Metrics) NotificationSent

func (m *Metrics) NotificationSent(
	ctx context.Context, category, channel string, seconds float64,
)

NotificationSent records how long one message took to leave.

func (*Metrics) OccurrenceMaterialized

func (m *Metrics) OccurrenceMaterialized(ctx context.Context, mode string, lagSeconds float64)

OccurrenceMaterialized records how late an occurrence was created against the moment it is for (the lag ADR-0008 promised).

The mode is the label because the two answer different questions: an ON_SCHEDULE series that is behind is a scheduler problem, while an ON_COMPLETION one is behind only for as long as nobody completed anything - which is a person's business rather than an operator's.

func (*Metrics) OutboundHTTP

func (m *Metrics) OutboundHTTP(ctx context.Context, targetClass string, seconds float64)

OutboundHTTP records the duration of one outbound call. targetClass is a class, never a host: a webhook target is chosen by a tenant, and a label per URL would grow a series per customer integration (§3.2, rule 10).

func (*Metrics) OutboxLag

func (m *Metrics) OutboxLag(ctx context.Context, seconds float64)

OutboxLag records the age of an event at delivery, which is what SLO-4 promises and what alert A-05 watches. Deliberately without a tenant label even when the tenant label is enabled: it is a histogram, and one per tenant multiplies buckets rather than series.

func (*Metrics) PanicRecovered

func (m *Metrics) PanicRecovered(ctx context.Context, component string)

PanicRecovered counts a caught panic. component is the location, not the message: a panic message can contain anything, including user content (rule 10).

func (*Metrics) PanicRecoveredBy

func (m *Metrics) PanicRecoveredBy(ctx context.Context, component string, delta int64)

PanicRecoveredBy exists for the zero seeding above; a delta of anything but 0 or 1 would be a misuse, and the counter is not the place to be clever.

func (*Metrics) PasswordRehashed

func (m *Metrics) PasswordRehashed(ctx context.Context)

PasswordRehashed counts one stored hash brought up to the current cost. No label: the counter shows how far an upgrade of the cost has reached, never whose.

func (*Metrics) PasswordResetRequested

func (m *Metrics) PasswordResetRequested(ctx context.Context)

PasswordResetRequested counts one forgotten-password request. No label: which ones reached an account is what the request must not reveal, and a counter split by it would.

func (*Metrics) PoolConnections

func (m *Metrics) PoolConnections(ctx context.Context, pool string, inUse, idle, maxConns int64)

PoolConnections publishes one pool's connection states. The max joins in_use and idle as a third state rather than a separate metric, because it is the denominator A-11 divides by and belongs on the same series a dashboard already reads. The pool label tells the API's pool from the background one - their saturation means different things (§6).

func (*Metrics) PullRecords

func (m *Metrics) PullRecords(ctx context.Context, count int)

PullRecords counts what a pull handed out. Beside the stream's counter rather than folded into it: the two together say whether devices keep up with the log, and apart, by which door.

func (*Metrics) PushResult

func (m *Metrics) PushResult(ctx context.Context, result string)

PushResult counts one mutation a push answered, by the contract's result - a closed set of four. What a device pushed and how much of it the server took is the one number that says whether offline work is landing.

func (*Metrics) QueueDepth

func (m *Metrics) QueueDepth(ctx context.Context, kind string, pending int64)

QueueDepth publishes the backlog of one kind. Only the scheduler leader reports it: the number describes the installation, and one written by every replica would be summed across instances and read as several times the truth.

func (*Metrics) QuotaUsage

func (m *Metrics) QuotaUsage(ctx context.Context, quota string, tenant string, ratio float64)

QuotaUsage records how close one workspace stands to one of its ceilings. The label is the quota - a bounded set of six - and the tenant joins it only when the operator enabled the tenant label (§3.2): without it the series carries the last workspace's number per quota, which is still the alarm A-18 needs ("somebody stands at 90%"), just not the who.

func (*Metrics) RateLimited

func (m *Metrics) RateLimited(ctx context.Context, scope string)

RateLimited counts a call turned away by a limit. scope says which limit: ip, token, tenant for the rate limiter, load_shed for the shedder, bulkhead:<compartment> for a full compartment. The set is written by hand and stays small - that is what keeps it a label.

func (*Metrics) ReminderFired

func (m *Metrics) ReminderFired(ctx context.Context, channel string, delaySeconds float64)

ReminderFired records how late one reminder was against the moment it promised (SLO-5).

The delay rather than the send: a reminder that left on time and then waited an hour for a mail server is a different failure, and the notification metrics already measure that one. What this answers is whether the schedule kept its promise.

func (*Metrics) RetentionBlocked

func (m *Metrics) RetentionBlocked(ctx context.Context, reason string, count int64)

RetentionBlocked counts what a pass kept, by reason. The reason is the point: "twelve were kept" is not something an operator can act on, and "twelve were kept by a legal hold" is.

func (*Metrics) RetentionDeleted

func (m *Metrics) RetentionDeleted(ctx context.Context, dataKind string, count int64)

RetentionDeleted counts what a pass removed. Written even when it is zero: a counter that has never been written has no series, and an alert on a deletion run that never happens is one that reads "no data" and is believed (observability-reliability.md §4).

func (*Metrics) RetentionRead

func (m *Metrics) RetentionRead(ctx context.Context, dataKind string, count int64)

RetentionRead counts what the rule sweep judged in a pass. Written even when it is zero, for the reason RetentionDeleted is.

func (*Metrics) RetentionRun

func (m *Metrics) RetentionRun(ctx context.Context, dataKind string, seconds float64)

RetentionRun records how long one pass took.

func (*Metrics) RuleDisabled

func (m *Metrics) RuleDisabled(ctx context.Context, reason string)

RuleDisabled counts one rule switching itself off. The reason is the engine's, and there is exactly one today; the label exists so a second protection would be told apart from the first.

func (*Metrics) RuleRun

func (m *Metrics) RuleRun(ctx context.Context, result, triggerType string)

RuleRun counts one ended automation run. Both values come from the engine's closed sets - the domain's status vocabulary and the six trigger kinds - never a rule or a tenant (§3.2).

func (*Metrics) SchedulerTickLag

func (m *Metrics) SchedulerTickLag(ctx context.Context, seconds float64)

SchedulerTickLag publishes how late the last tick was. Seconds as a whole number: the gauge exists to show drift, and a schedule that is off by less than a second is not drifting.

func (*Metrics) SecretResealed

func (m *Metrics) SecretResealed(ctx context.Context, store, outcome string, count int64)

SecretResealed counts what one re-seal pass did in one store. Both labels are closed sets written by hand: the store is one of the five places a sealed value lives, the outcome is `rewrapped` or `skipped` - the latter a value naming a key the ring no longer holds, which is the state the operator reads from the census rather than from here.

func (*Metrics) Shutdown

func (m *Metrics) Shutdown(ctx context.Context) error

Shutdown flushes the meter. Metrics missing from the last seconds of a pod's life are exactly the ones an incident review wants.

func (*Metrics) SignInStep

func (m *Metrics) SignInStep(ctx context.Context, step string)

SignInStep counts one step a sign-in was handed, by step - a closed set, never a subject.

func (*Metrics) StreamClosed

func (m *Metrics) StreamClosed(ctx context.Context, seconds float64)

StreamClosed gives the connection back and records how long it lasted.

func (*Metrics) StreamOpened

func (m *Metrics) StreamOpened(ctx context.Context)

StreamOpened counts a connection this process has taken on.

func (*Metrics) StreamRecords

func (m *Metrics) StreamRecords(ctx context.Context, count int)

StreamRecords counts what went down the streams. Against the change log's own growth it is what says whether the streams are keeping up.

func (*Metrics) StreamRefused

func (m *Metrics) StreamRefused(ctx context.Context, reason string)

StreamRefused counts a connection that was not taken on, by the limit that refused it. The reason is the point: "twelve refused" is not something an operator can act on, and "twelve refused by the per-credential cap" is.

func (*Metrics) UseCase

func (m *Metrics) UseCase(ctx context.Context, useCase, result string, tenant string)

UseCase records the outcome of a use case. The result is the error category, not the message: ok, or one of the domain's categories in lower case (§4.1) - see ResultClass, which is the only thing that should be producing this value.

func (*Metrics) WebhookDelivery

func (m *Metrics) WebhookDelivery(ctx context.Context, result, statusClass string)

WebhookDelivery counts one settled attempt. Both labels are closed sets the deliverer owns: what became of the attempt, and the class of the answer - never a target, never a tenant (§3.2, rule 10: a label per URL would grow a series per customer integration).

func (*Metrics) WebhookRetryBacklog

func (m *Metrics) WebhookRetryBacklog(ctx context.Context, waiting int64)

WebhookRetryBacklog publishes how many deliveries wait for their next rung of the ladder.

type Observer

type Observer struct {
	// contains filtered or unexported fields
}

Observer gives one use case execution its metric and its span - the two signals the Definition of Done requires of every use case, and what gate RT-12 checks for (ADR-0016 item 6).

It is a wrapper rather than two calls at each call site, because two calls are two chances to forget one, and the one that gets forgotten is the error path.

func NewObserver

func NewObserver(metrics *Metrics, tracing *Tracing) *Observer

func (*Observer) Job

func (o *Observer) Job(ctx context.Context, kind string, fn func(context.Context) error) error

Job runs one job execution as an observed unit (ADR-0008, §3.3).

It is the queue's counterpart to UseCase, and it exists for the same reason: a job that produced no span is a job nobody can follow through the pipeline dashboard, and the failure that most needs following is the one that happened at three in the morning.

What it does not do yet is continue the trace of whatever caused the job. §3.3 wants the traceparent persisted in the event and the job row, so that a chain of HTTP request -> event -> automation rule -> webhook is one trace; that needs a column and belongs with the dispatcher work in 0.5.0. Until then a job span is its own root, which is enough to see what a job did and how long it took, and not enough to see what asked for it.

func (*Observer) Registry

func (o *Observer) Registry() usecase.Middleware

Registry returns the middleware the use case catalogue is built with.

This is what makes gate RT-12 structural rather than a rule people remember: the composition root passes it to usecase.NewRegistry, every entry's handler is wrapped on the way in, and there is then no path to a use case that produces neither a metric nor a span - whether the call arrived through REST, through MCP, or from an automation rule.

func (*Observer) UseCase

func (o *Observer) UseCase(ctx context.Context, name string, fn func(context.Context) error) error

UseCase runs fn as one observed use case. name is the registry name (CreateContainer), never anything derived from input - it is a metric label and a span name.

type Tracing

type Tracing struct {
	Enabled bool
	// contains filtered or unexported fields
}

Tracing owns the tracer provider. It exists whether or not tracing is enabled: the W3C propagator is installed either way, so an incoming traceparent still reaches the log and still travels onwards to the next service (§3.3). Turning tracing off means "export nothing", not "lose the correlation".

func NewTracing

func NewTracing(ctx context.Context, cfg env.Config) (*Tracing, error)

NewTracing builds the tracer provider from the configuration and installs it globally, along with the W3C trace context and baggage propagators.

Disabled is the default and the documented self-hosting profile (§13): a no-op provider, no exporter, no goroutine, and no cost per span - but the propagator is still there.

func (*Tracing) Shutdown

func (t *Tracing) Shutdown(ctx context.Context) error

Shutdown flushes what has not been exported yet. Without it the spans of the last seconds of a pod's life are lost - which are the ones an incident review wants.

func (*Tracing) Tracer

func (t *Tracing) Tracer(name string) trace.Tracer

Tracer hands out a named tracer. Named per component, so a backend can tell a span raised by the REST layer from one raised by a job.

Jump to

Keyboard shortcuts

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