Documentation
¶
Overview ¶
Package config defines the optic.yaml schema, its loader, and validation.
Design notes:
- The schema is intentionally declarative and order-sensitive: rules are evaluated top-to-bottom and *merged* (not first-match-wins), so a broad redaction rule and a narrow restriction rule can both apply.
- Capture semantics are OPT-OUT: everything is captured unless a rule's `restrict` list disables it.
- Validation is strict and happens once at load time so the hot path (per-request rule evaluation) never has to handle malformed input.
Index ¶
- Constants
- Variables
- func Bool(v *bool) bool
- func LevelRankKnown(level string) int
- type AdminAuth
- type AdminTLS
- type AppLogRedact
- type AppLogSource
- type AppLogsCfg
- type Billing
- type CaptureField
- type CaptureFlags
- type Config
- type Defaults
- type DetectorCfg
- type ExporterCfg
- type LabelSource
- type Match
- type Metrics
- type Prices
- type Redact
- type Rule
- type RuleLogs
- type ScanCfg
- type Service
- type SpanRedact
- type SpansCfg
- type StoreCfg
- type Telemetry
- type TraceCfg
Constants ¶
const DefaultCaptureLimitBytes = 64 * 1024
const MaxAnalysisRows = 500_000
MaxAnalysisRows mirrors store.MaxAnalysisMaxRows. Duplicated rather than imported to keep config free of a dependency on store.
Variables ¶
var FirstStringFunc func(node any, path []string) (string, bool) = func(any, []string) (string, bool) { return "", false }
FirstStringFunc is wired to engine.FirstString at init to avoid an import cycle: config owns the schema, engine owns the JSON walker.
Functions ¶
func LevelRankKnown ¶ added in v0.9.0
LevelRankKnown returns the severity rank of a level, or -1 if it is not one of the recognised names. Used to reject a typo in level_min at load time — "warining" would otherwise silently keep everything.
Types ¶
type AdminAuth ¶
type AdminAuth struct {
// Token is a bearer token compared in constant time. Prefer TokenEnv:
// a secret in a config file is a secret in your git history.
Token string `yaml:"token"`
// TokenEnv names an environment variable holding the token. It wins
// over Token when both are set.
TokenEnv string `yaml:"token_env"`
// AllowHealth keeps /healthz unauthenticated so orchestrator probes
// keep working. Default true.
AllowHealth *bool `yaml:"allow_health"`
}
AdminAuth protects the control plane. It is off by default because the admin port is meant to be firewalled, but "meant to be" is not a control — enable this whenever the port could be reachable.
func (*AdminAuth) HealthOpen ¶
HealthOpen reports whether /healthz bypasses authentication.
type AppLogRedact ¶ added in v0.9.0
type AppLogSource ¶ added in v0.11.0
type AppLogSource struct {
// Type is "stdout" (the child process started with `optictrace run -exec`)
// or "file".
Type string `yaml:"type"`
// Path is the file to tail, for type: file. Rotation is followed.
Path string `yaml:"path"`
// Service overrides the service name attributed to these lines. Defaults
// to service.name.
Service string `yaml:"service"`
// Format is "json" (the default) or "text". A text line has no fields and
// no span of its own unless SpanPattern finds one, so text sources mostly
// produce orphans — which is worth knowing before choosing the format
// rather than after seeing an empty dashboard.
Format string `yaml:"format"`
// Field names to read a JSON line's parts from. Empty values fall back to
// the conventional names, so a structured logger usually needs no mapping.
TraceField string `yaml:"trace_field"` // default: trace_id
SpanField string `yaml:"span_field"` // default: span_id
LevelField string `yaml:"level_field"` // default: level
MessageField string `yaml:"message_field"` // default: message, then msg
RouteField string `yaml:"route_field"` // default: route
// SpanPattern extracts a span id from a line that has no structured field
// for it — a regex with one capture group. The escape hatch for text logs
// and for loggers that only interpolate the id into the message.
SpanPattern string `yaml:"span_pattern"`
}
AppLogSource is one place to read application log lines from.
type AppLogsCfg ¶ added in v0.9.0
type AppLogsCfg struct {
Enabled bool `yaml:"enabled"`
// LevelMin drops anything less severe before it is stored. Most of the
// volume — and almost none of the value — is debug lines.
LevelMin string `yaml:"level_min"`
// MaxLinesPerSpan caps one request's contribution. A retry loop logging
// inside a hot path can otherwise write millions of lines against a
// single span. 0 uses the default; -1 means no cap.
MaxLinesPerSpan int `yaml:"max_lines_per_span"`
// MaxMessageBytes truncates an individual line. Stack traces are large
// and the first lines are the ones that matter.
MaxMessageBytes int `yaml:"max_message_bytes"`
// DropOrphans discards lines that carry no span id — startup, cron jobs,
// background workers. Default true: they cannot be attributed to a
// request, and attaching them to whichever request happened to be in
// flight would cross-attribute tenants.
//
// Whatever this is set to, drops are counted in
// optictrace_app_logs_dropped_total — data thrown away silently is data
// nobody knows they are missing.
DropOrphans *bool `yaml:"drop_orphans"`
// RetentionMaxAge expires lines independently of records. App logs run
// orders of magnitude above request volume and are rarely wanted for as
// long.
RetentionMaxAge time.Duration `yaml:"retention_max_age"`
// Redact scrubs lines before they are stored. Patterns are regexes
// applied to the message and to every structured field value; each match
// is replaced with [REDACTED].
Redact AppLogRedact `yaml:"redact"`
// Sources collect lines the application already writes, instead of
// requiring it to POST them. An app that logs JSON to stdout needs no code
// change at all — which matters, because the services whose logs you most
// want are usually the ones nobody wants to modify.
Sources []AppLogSource `yaml:"sources"`
}
AppLogsCfg governs application log lines — the highest-risk surface in the product. An app log routinely contains bearer tokens, email addresses and whole payloads inside stack traces, so lines are redacted and capped on the way in rather than stored raw and cleaned up later.
func (*AppLogsCfg) DropOrphanLines ¶ added in v0.9.0
func (a *AppLogsCfg) DropOrphanLines() bool
DropOrphanLines reports whether uncorrelated lines are discarded.
type Billing ¶
type Billing struct {
// ConsumerLabel is the optic.yaml label that identifies the consumer
// (must be declared under some rule's `labels`). Default "tenant".
ConsumerLabel string `yaml:"consumer_label"`
Currency string `yaml:"currency"` // display only; default "USD"
Prices Prices `yaml:"prices"`
}
Billing turns telemetry into cost attribution (FinOps): usage is grouped by one consumer label (e.g. tenant) and priced by the models below. All prices are optional — omitted ones simply contribute zero.
type CaptureField ¶
type CaptureField string
CaptureField enumerates the telemetry channels a rule may restrict.
const ( FieldRequestBody CaptureField = "request_body" FieldResponseBody CaptureField = "response_body" FieldHeaders CaptureField = "headers" FieldQuery CaptureField = "query" )
type CaptureFlags ¶
type CaptureFlags struct {
RequestBody *bool `yaml:"request_body"`
ResponseBody *bool `yaml:"response_body"`
Headers *bool `yaml:"headers"`
Query *bool `yaml:"query"`
}
CaptureFlags uses *bool so an omitted key ("capture everything") is distinguishable from an explicit `false`.
type Config ¶
type Config struct {
Version int `yaml:"version"`
Service Service `yaml:"service"`
Defaults Defaults `yaml:"defaults"`
Telemetry Telemetry `yaml:"telemetry"`
Scan ScanCfg `yaml:"scan"`
Rules []Rule `yaml:"rules"`
}
Config is the root of an optic.yaml document.
func (*Config) Detectors ¶ added in v0.8.0
Detectors compiles the configured detectors. Validate has already proved they compile, so an error here means the config was mutated after loading.
func (*Config) RequireProxyAddrs ¶ added in v0.8.0
RequireProxyAddrs enforces the invariants of sidecar mode, where a listener is actually opened. It is separate from Validate because embedded middleware and the analysis subcommands legitimately have neither address.
The failure this prevents: an omitted `listen` reaches net/http as Addr:"" and binds port 80 — either quietly serving where nobody expects it, or failing with "listen tcp :80: bind: permission denied", a port number that appears nowhere in the user's config and gives them nothing to search for.
func (*Config) RestartRequired ¶ added in v0.8.0
RestartRequired lists the settings that differ between two configs but cannot be applied by a hot reload, which only swaps the rule engine and the metrics label schema.
Reload used to parse the whole file, validate it, apply the rules, and silently discard everything else — reporting success either way. Someone changing an exporter and reloading had no way to learn it had not taken effect. Naming the fields is most of the fix.
type Defaults ¶
type Defaults struct {
Capture CaptureFlags `yaml:"capture"`
CaptureLimitBytes int64 `yaml:"capture_limit_bytes"`
}
Defaults holds the global capture posture applied before any rule.
type DetectorCfg ¶ added in v0.8.0
type DetectorCfg struct {
Kind string `yaml:"kind"` // finding label, e.g. "aadhaar"
Severity string `yaml:"severity"` // critical | high | medium
Why string `yaml:"why"` // what a reader should understand about the risk
Pattern string `yaml:"pattern"` // Go regexp
// Verify names a built-in checksum to confirm a regex hit — "luhn",
// "iban", "us_ssn", "verhoeff". Strongly preferred over a bare pattern:
// a detector that cries wolf gets switched off, which is worse than not
// having it. Empty means no checksum.
Verify string `yaml:"verify"`
}
DetectorCfg is one user-defined sensitive-value pattern.
type ExporterCfg ¶
type ExporterCfg struct {
Name string `yaml:"name"` // unique; becomes the Prometheus `exporter` label
Type string `yaml:"type"` // "file" | "webhook" | "command" | "otlp"
// file: JSONL append target, rotated at MaxSizeMB (default 100).
Path string `yaml:"path"`
MaxSizeMB int `yaml:"max_size_mb"`
// webhook: POST a JSON array of records to URL with optional headers.
URL string `yaml:"url"`
Headers map[string]string `yaml:"headers"`
// command: the custom-plugin hook. OpticTrace spawns this executable and
// streams one JSON record per line to its stdin — write a plugin in any
// language to ship data to Kafka, S3, a SIEM, anywhere. The process is
// restarted with backoff if it exits.
Command []string `yaml:"command"`
// Batching (file & webhook): flush when BatchSize records accumulate or
// FlushInterval elapses, whichever comes first.
BatchSize int `yaml:"batch_size"` // default 50
FlushInterval string `yaml:"flush_interval"` // Go duration, default "3s"
QueueSize int `yaml:"queue_size"` // per-exporter; default 1024
// Settings carries configuration for an out-of-tree exporter registered
// via ext.RegisterExporter. The decoder rejects unknown top-level keys on
// purpose — a typo must fail loudly rather than look like a working rule —
// so a plugin's own keys need somewhere legitimate to live, and this is it.
Settings ext.Settings `yaml:"settings"`
}
ExporterCfg declares one output plugin. Every exporter receives the SAME governed records the store does — post-restriction, post-redaction — so no export path can ever see raw sensitive data.
func (*ExporterCfg) FlushEvery ¶
func (e *ExporterCfg) FlushEvery() time.Duration
FlushEvery returns the parsed flush interval (validated at load time).
type LabelSource ¶ added in v0.9.0
type LabelSource struct {
Kind string // "header" | "query" | "path" | "static" | "json" | "json_response"
Key string // header/query name, the literal value for static
Seg int // 1-indexed path segment when Kind == "path"
// JSONPath is the split body path for the json kinds.
JSONPath []string
// Extract, when set, pulls a capture group out of the raw value. Exactly
// one group; a non-match yields "" rather than the whole value, so a
// mismatched pattern produces a missing label instead of a surprising one.
Extract *regexp.Regexp
}
LabelSource describes where a custom label's value comes from.
Compiled once at load time: the hot path does no parsing, and a malformed source or regex fails `optictrace validate` rather than at request time.
func ParseLabelSource ¶ added in v0.9.0
func ParseLabelSource(src string) (LabelSource, error)
ParseLabelSource compiles a label source expression:
header:X-Tenant-ID query:tenant path:4 1-indexed segment static:premium
optionally followed by |<regex> with exactly one capture group.
Exported so config validation and the engine use the same parser — two implementations of a grammar drift, and the drift shows up as a rule that validates but does not fire.
func (LabelSource) NeedsBody ¶ added in v0.9.0
func (s LabelSource) NeedsBody() bool
NeedsBody reports whether this source reads a request or response body.
func (LabelSource) NeedsResponseBody ¶ added in v0.9.0
func (s LabelSource) NeedsResponseBody() bool
NeedsResponseBody reports whether this source reads the RESPONSE body, which is only available after the handler has run.
func (LabelSource) Value ¶ added in v0.9.0
func (s LabelSource) Value(r *http.Request) string
Extract pulls the label value out of a request. Missing values return "". Value resolves this source against a request.
func (LabelSource) ValueFromBody ¶ added in v0.9.0
func (s LabelSource) ValueFromBody(doc any) string
ValueFromBody resolves a json source against an already-parsed body.
The body handed here is the GOVERNED one — post-redaction. That is deliberate and load-bearing: extracting from the raw payload would let `labels: {email: "json:$.customer.email"}` copy a redacted value into a Prometheus label and the stored labels map, quietly routing around the governance the rule next to it is enforcing. Extracting after redaction means such a label reads "[REDACTED]" — visibly wrong rather than silently leaking. Validate also refuses the overlap outright.
type Match ¶
type Match struct {
Path string `yaml:"path"`
Methods []string `yaml:"methods"`
// GraphQLOperation matches the operation name inside a GraphQL request
// body, as a glob. Every GraphQL operation POSTs to the same path, so
// without this a rule cannot target one: you could not redact a field on
// createPaymentMethod without applying the same rule to every query.
//
// Requires service.graphql_paths to include the route, since extracting
// the name means reading the request body before the response.
GraphQLOperation string `yaml:"graphql_operation"`
// Headers matches request headers by regular expression: the rule applies
// only when EVERY listed header matches its pattern. Header names are
// case-insensitive, as HTTP requires.
//
// match:
// path: "/api/**"
// headers:
// X-Plan: "^(gold|platinum)$"
//
// Patterns are unanchored by default, exactly like Go's regexp — write ^
// and $ when you mean a whole-value match. `"."` therefore means "the
// header is present and non-empty", which is a useful idiom.
Headers map[string]string `yaml:"headers"`
// Query matches query parameters the same way.
Query map[string]string `yaml:"query"`
// Body matches values inside the JSON request body by path, again as
// regular expressions:
//
// match:
// path: "/api/v1/leads"
// body:
// "$.**.source": "^flipkart$"
//
// This is what distinguishes callers that are otherwise identical — the
// same endpoint, the same tenant, the same product, differing only in a
// field of the payload.
//
// It costs a buffered request body on the matching routes, which is why
// it is per-rule rather than global: only paths with a body rule pay.
// Matched against the GOVERNED body, so a redacted field cannot be used
// as a criterion — see the note on json label sources.
Body map[string]string `yaml:"body"`
}
Match selects requests by path glob and (optionally) HTTP methods.
type Metrics ¶
type Metrics struct {
Enabled *bool `yaml:"enabled"` // default true
// Buckets are latency histogram bounds in seconds. Defaults tuned for
// API traffic (1ms .. 10s).
Buckets []float64 `yaml:"buckets"`
// MaxLabelValues caps how many DISTINCT values each custom label may
// contribute to metrics. Label values come from arbitrary request
// headers, so one buggy or hostile client can otherwise blow up
// Prometheus cardinality. Beyond the cap, values collapse into
// "__over_limit__" and optictrace_label_capped_total increments.
// Route cardinality is already bounded by design; this closes the same
// hole for custom labels. Default 500; an explicit 0 disables the guard.
MaxLabelValues *int `yaml:"max_label_values"`
}
Metrics controls the Prometheus exporter.
func (*Metrics) LabelValueCap ¶
LabelValueCap resolves the cardinality guard (0 = disabled).
type Redact ¶
type Redact struct {
Headers []string `yaml:"headers"`
JSONFields []string `yaml:"json_fields"`
// QueryParams are masked in the captured query string. Credentials in
// query strings are common (?api_key=…), so capturing queries without
// a way to mask them would be a governance regression.
QueryParams []string `yaml:"query_params"`
}
Redact lists what to mask in *captured* telemetry. The proxied traffic itself is never modified.
type Rule ¶
type Rule struct {
Name string `yaml:"name"`
Match Match `yaml:"match"`
Restrict []CaptureField `yaml:"restrict"`
Redact *Redact `yaml:"redact"`
// Labels attach dimensions to this request's telemetry: Prometheus label
// values, stored record fields, and the grouping key for usage and cost
// attribution. Each value is a source expression:
//
// header:X-Tenant-ID the request header
// query:tenant a query parameter
// path:4 the 4th path segment, 1-indexed
// static:premium a constant — the way to TAG a class of traffic
//
// Any source may be followed by |<regex> to extract part of the value.
// The regex needs exactly one capture group, and that group becomes the
// label; a non-match yields an empty label rather than the whole value.
//
// region: "header:X-Region|^([a-z]{2})-" eu-west-1 -> eu
//
// Rules merge top to bottom and later rules win, so conditional tagging
// needs no separate mechanism: give a broad rule a static default and let
// a narrower rule with `match.headers` override it.
//
// Label values are client-controlled, so they pass through the metrics
// cardinality guard (telemetry.metrics.max_label_values) before becoming
// Prometheus dimensions.
Labels map[string]string `yaml:"labels"`
// Sample captures bodies for only this fraction of matched requests
// (0..1]. Metrics and metadata are always recorded in full — sampling
// only bounds payload volume on hot routes. Later rules override.
Sample *float64 `yaml:"sample"`
// KeepErrors and KeepSlowerThan are TAIL-BASED sampling: they rescue
// interesting requests that uniform `sample` would have thrown away.
// The decision is made after the response, so a route using either one
// buffers bodies for every request and discards them at the end.
KeepErrors *bool `yaml:"keep_errors"` // always capture status >= 500
KeepSlowerThan string `yaml:"keep_slower_than"` // Go duration, e.g. "500ms"
// Meter extracts numeric usage figures from RESPONSE bodies by JSON
// path — e.g. tokens: "$.usage.total_tokens" for LLM APIs. Values are
// summed per consumer for usage/cost attribution. Metering is
// independent of capture rules: a restricted route can still meter.
Meter map[string]string `yaml:"meter"`
// Logs narrows the application-log policy for requests this rule matches.
//
// telemetry.app_logs sets the floor for every route; this tightens it per
// route, which is the shape the risk actually has. A payments handler
// deserves a stricter level floor and extra redaction than a health check,
// and expressing that globally means applying the strictest setting
// everywhere — which in practice means people set it loosely.
//
// Only ever tightens. A rule cannot raise a cap or lower a level floor
// below the global one: a per-route override that could weaken the global
// policy would make the global setting a suggestion rather than a
// guarantee, and reviewing one file would no longer tell you what is
// enforced.
Logs *RuleLogs `yaml:"logs"`
}
Rule couples a traffic matcher with governance actions.
func (*Rule) SlowerThan ¶
SlowerThan returns the parsed tail-sampling latency threshold (0 = unset). Validated at load time, so parse errors are impossible here.
type RuleLogs ¶ added in v0.11.0
type RuleLogs struct {
// LevelMin raises the severity floor for this route. Ignored if it would
// LOWER the global floor.
LevelMin string `yaml:"level_min"`
// MaxLinesPerSpan lowers the per-request line cap for this route. Ignored
// if it would raise the global cap. -1 is not accepted here: removing the
// cap is a global decision.
MaxLinesPerSpan int `yaml:"max_lines_per_span"`
// Redact adds patterns and field names on top of the global set. Additive
// only — there is no way to remove a global redaction, because a rule that
// could unmask something would make the global list unreviewable.
Redact AppLogRedact `yaml:"redact"`
// Drop discards application log lines for this route entirely. The
// honest way to say "never store what this handler logs" — a debug
// endpoint, or a route whose logs are known to carry secrets nothing can
// pattern-match reliably.
Drop bool `yaml:"drop"`
}
RuleLogs is the per-rule application-log policy. Every field is optional; an omitted field inherits telemetry.app_logs.
type ScanCfg ¶ added in v0.8.0
type ScanCfg struct {
// Detectors are org-specific patterns, added to the built-in set rather
// than replacing it. The built-ins cover credentials and regulated
// identifiers that look the same everywhere; these cover the ones that
// do not — internal employee IDs, customer account formats, national
// identifiers outside the US.
Detectors []DetectorCfg `yaml:"detectors"`
}
ScanCfg configures `optictrace scan`, the leak detector that looks for sensitive values which slipped past governance.
type Service ¶
type Service struct {
Name string `yaml:"name"`
Listen string `yaml:"listen"`
Upstream string `yaml:"upstream"`
// GraphQLPaths lists path globs served by GraphQL. On these routes the
// request body is parsed for an operation name, which then becomes part
// of the route label and can be matched by a rule.
//
// Opt-in because it means buffering the request body on those routes.
// The alternative — every operation collapsing into one `/graphql` route
// — makes latency percentiles, per-operation rules and spec inference
// meaningless for a GraphQL service.
GraphQLPaths []string `yaml:"graphql_paths"`
// Trace controls W3C Trace Context handling.
Trace TraceCfg `yaml:"trace"`
// HTTP2 serves cleartext HTTP/2 (h2c) on the proxy listener in addition
// to HTTP/1.1. Off by default: it changes protocol negotiation for every
// client, and HTTP/1.1 is what most upstreams speak.
//
// This is what an HTTP/2 client needs in order to connect at all — but it
// is NOT gRPC support. gRPC bodies are length-prefixed protobuf frames,
// so without message descriptors the governance engine cannot match
// fields, redact them, or meter them; you would get method names and byte
// counts. Use the SDK middleware for gRPC services.
HTTP2 bool `yaml:"http2"`
}
Service describes the proxied service (standalone sidecar mode). When OpticTrace is embedded as middleware, Listen/Upstream are unused.
type SpanRedact ¶ added in v0.15.0
type SpanRedact struct {
// Patterns are regexes. A pattern that fails to compile is a config
// error, not a silently-skipped rule.
Patterns []string `yaml:"patterns"`
// Fields are attribute keys whose values are replaced wholesale.
Fields []string `yaml:"fields"`
}
SpanRedact mirrors AppLogRedact deliberately: one shape for "scrub this free text", so nobody has to learn two.
type SpansCfg ¶ added in v0.15.0
type SpansCfg struct {
Enabled bool `yaml:"enabled"`
// MinDuration drops operations faster than this. A 20µs cache hit
// repeated a thousand times is volume without information, and the point
// of a breakdown is almost always the slow thing. 0 keeps everything.
MinDuration time.Duration `yaml:"min_duration"`
// MaxPerRequest caps one request's contribution. An N+1 query inside a
// loop can otherwise write thousands of spans against a single request —
// and it is worth noting that the cap being HIT is itself the finding, so
// it is counted rather than silent. 0 uses the default; -1 means no cap.
MaxPerRequest int `yaml:"max_per_request"`
// MaxAttrBytes truncates an individual attribute value and the error
// text. Statements and stack traces are long, and their beginnings are
// the parts that identify them.
MaxAttrBytes int `yaml:"max_attr_bytes"`
// DropOrphans discards spans carrying no parent span id — work done
// outside any request, such as a scheduled job. Default true: they cannot
// be attributed to a request, and attaching them to whichever request
// happened to be in flight would cross-attribute tenants.
//
// Whatever this is set to, drops are counted in
// optictrace_spans_dropped_total.
DropOrphans *bool `yaml:"drop_orphans"`
// RetentionMaxAge expires spans independently of records. Spans run well
// above request volume and are rarely wanted for as long.
RetentionMaxAge time.Duration `yaml:"retention_max_age"`
// Redact scrubs spans before they are stored. Patterns are regexes
// applied to every attribute value and to the error text; Fields are
// attribute keys whose values are replaced wholesale.
Redact SpanRedact `yaml:"redact"`
}
AppLogRedact is the log-line equivalent of a rule's redact block. SpansCfg governs inner spans on the way into the store.
The attributes are the risk, not the timings: a statement quotes its parameters, a cache key embeds an account id, an outbound URL carries a token in its query string. So the same treatment as app logs — scrubbed and capped BEFORE persistence, because "clean it up later" is after the data is already at rest.
func (*SpansCfg) DropOrphanSpans ¶ added in v0.15.0
DropOrphanSpans reports whether spans with no parent should be discarded. Defaults to true when unset: a span that belongs to no request cannot be attributed, and attaching it to whichever request happened to be in flight would cross-attribute tenants.
type StoreCfg ¶
type StoreCfg struct {
// Driver is "sqlite" (default), "postgres" (multi-node),
// "clickhouse" (column store, for volume), or "none".
Driver string `yaml:"driver"`
// DSN is the SQLite file path, or a postgres:// / clickhouse:// URL.
// Default "optictrace.db".
DSN string `yaml:"dsn"`
// QueueSize bounds the async write queue; writes are dropped (and
// counted) rather than ever blocking the request path. Default 4096.
QueueSize int `yaml:"queue_size"`
// RetentionMaxRows caps the log table; oldest rows are pruned.
// 0 disables pruning. Default 100000.
RetentionMaxRows int64 `yaml:"retention_max_rows"`
// RetentionMaxAge deletes records older than this regardless of row
// count — the control a data-retention policy is actually written in
// ("keep 30 days"). Go duration, e.g. "720h". Empty disables it.
RetentionMaxAge string `yaml:"retention_max_age"`
// Settings carries configuration for an out-of-tree store registered via
// ext.RegisterStore. See ExporterCfg.Settings for why this exists.
Settings ext.Settings `yaml:"settings"`
// AnalysisMaxRows bounds how many records one analysis pass reads —
// `scan`, `spec`, `suggest`, `review`, `replay` and the /api/scan and
// /api/spec endpoints. These read FULL records including bodies, so at
// the default capture limit this is a memory ceiling rather than just a
// row count. Default 20000; hard ceiling 500000.
AnalysisMaxRows int `yaml:"analysis_max_rows"`
}
StoreCfg configures asynchronous payload persistence.
type Telemetry ¶
type Telemetry struct {
// AdminListen is the address of the admin server (/metrics, dashboard,
// query APIs). Kept separate from proxied traffic on purpose: you can
// firewall it independently.
//
// Default "127.0.0.1:9095" — loopback, NOT all interfaces. The admin API
// can read every captured payload, so exposing it is a deliberate act:
// set "0.0.0.0:9095" explicitly (and turn on `auth`) when you mean it.
AdminListen string `yaml:"admin_listen"`
// CORSOrigins allows listed browser origins to call the admin API
// cross-origin — normally just a dashboard dev server, e.g.
// "http://localhost:3001". Empty (the default) sends no CORS headers at
// all, so the browser same-origin policy protects the API even when auth
// is off. "*" is accepted but rejected by Validate unless auth is
// enabled, because a wildcard plus no credentials lets any page a
// developer visits read the entire capture store.
CORSOrigins []string `yaml:"cors_origins"`
ConsoleLog *bool `yaml:"console_log"` // structured stdout telemetry (default true)
Metrics Metrics `yaml:"metrics"`
Store StoreCfg `yaml:"store"`
Exporters []ExporterCfg `yaml:"exporters"`
Billing *Billing `yaml:"billing"`
// AppLogs governs application log lines correlated to spans. Nil means
// the feature is off and the ingest endpoint refuses politely.
AppLogs *AppLogsCfg `yaml:"app_logs"`
// Spans governs inner spans — the database queries, cache lookups and
// outbound calls that happened while a request was being served. Nil
// means the feature is off and the ingest endpoint refuses politely.
Spans *SpansCfg `yaml:"spans"`
Auth *AdminAuth `yaml:"auth"`
TLS *AdminTLS `yaml:"tls"`
}
Telemetry configures the observability sinks: the admin/metrics endpoint, Prometheus exposition, console logging, and the payload store.
func (*Telemetry) AdminReachable ¶ added in v0.8.0
AdminReachable reports whether AdminListen accepts connections from beyond loopback. Used to decide how loudly to warn about an unauthenticated port.
type TraceCfg ¶ added in v0.9.0
type TraceCfg struct {
// PropagateUpstream sets traceparent on the request FORWARDED to your
// service, carrying this hop's span id so downstream calls become its
// children.
//
// Default true. This is the one deliberate exception to "live traffic is
// never modified", and it is narrow: the forwarded copy only — never the
// response, never what the client sent.
//
// It rewrites an inbound traceparent rather than only filling in a
// missing one. Passing the caller's header through unchanged would make
// every downstream hop a sibling of this one rather than a child, so the
// fan-out flattens into a list and the tree is lost. An application doing
// its own tracing nests under this span, which is correct.
//
// Set false to keep the forwarded request byte-identical; correlation
// then stops at whatever the application itself propagates.
PropagateUpstream *bool `yaml:"propagate_upstream"`
// ResponseHeader, when set, returns the trace id to the CALLER under this
// header name — "X-Trace-Id" is conventional. Off by default because it
// modifies the response, which nothing else here does. Worth turning on
// if you want support tickets to arrive with a trace id in them.
ResponseHeader string `yaml:"response_header"`
}
TraceCfg controls correlation across services.
OpticTrace always RECORDS trace ids — that costs nothing and is what turns records from several services into one request. What is configurable is whether it writes a header anywhere, because writing one modifies traffic and this product's central promise is that it does not.