Simple Agility Telemetry Contract

This is the in-contract of the Simple Agility observability plane. It says what a product, internal-facing or external-facing, must emit so that jakemon can capture it and so that the product's own error and behavioural signals come back to it aggregated. Read it before you write a line of logging in a new product. It is deliberately small. For logs it adopts a published standard, Elastic Common Schema (ECS), and requires only a handful of its fields; see Evolution rules below.

Four parts: metrics, logs, events, health. Conformance is machine-checkable. Run bin/simple-agility-conformance.sh (Wikantik repository) against an instance rather than reading the checklist and forming an opinion.

The decisions behind this page, with their reasons, are in the SimpleAgilityDecisionLog. The ECS logging standard is SA-6, and product-instance naming is SA-5. Each team's concrete steps to get there, and their status, are on SimpleAgilityTelemetryAdoption.

1. Metrics: expose /metrics

prometheus.scrape "roller" {
  targets = [{
    __address__      = "127.0.0.1:8080",
    __metrics_path__ = "/metrics",
    service          = "roller",
    service_instance = "roller-prod",
  }]()
  forward_to = [prometheus.remote_write.central.receiver]
  job_name   = "roller"
}
LabelMeaningExample
serviceThe product. It matches the metric prefix and the service.name log field.wikantik
service_instanceThis running copy of the product, named <product>-<env>: lowercase, joined by a hyphenwiki-prod, wiki-support, wiki-dev, roller-prod
jobThe scrape job, set in apps.alloy. For a product it is the product name.wikantik
instanceThe scrape target, which is Prometheus's own meaning of the labelhost.docker.internal:8080
hostThe fleet hostdocker1
deploymentThe whole stack deployment: one tenant's jakemon, Wikantik and Rollerjakemon-home

2. Logs: ECS JSON on stdout, one line per event

Write logs to stdout. The path is container runtime, then Alloy, then Loki. Use no log files, and no product-side log tables for aggregation. Emit one JSON object per line, using Elastic Common Schema field names. Dotted keys may be flat ("log.level": "info", as log4j2's ECS layout writes them) or nested ("log": {"level": "info"}), because LogQL reads both the same way.

Canonical line

{"@timestamp":"2026-10-12T06:14:22.481Z","log.level":"info","message":"assembled context bundle","ecs.version":"1.2.0","service.name":"wikantik","event.action":"bundle_assembled","trace.id":"4bf92f3577b34da6a3ce929d0e0e4736","transaction.id":"7f3c1a90","log.logger":"com.wikantik.bundle.Assembler","wikantik.query":"hybrid retrieval","wikantik.sections":12,"event.duration":184000000}

The first four fields are required. The rest are either ECS fields the contract defines below, or the service's own additions under its own namespace (wikantik.*), which need no permission from anybody.

The required and contract-defined fields

ECS fieldRequired?Meaning
@timestamprequiredISO-8601 timestamp, in UTC (Z) or with an offset
log.levelrequireddebug, info, warn or error. Consumers compare it case-insensitively, because log4j2 writes INFO.
messagerequiredHuman-readable message
service.namerequiredThe product name, matching the metric prefix and the plane's service label (wikantik, roller)
ecs.versionrecommendedWhatever the ECS layout emits. It is informational and not checked.
event.actionwhen the line is an eventA machine-stable event name in snake_case, product-prefixed if it might collide (page_view, email_bounced). See §3.
trace.idwhen knownThe correlation identifier: travels unchanged across every hop of one flow through the stack. See below.
transaction.idinside a requestA per-hop identifier, minted by this service for this request
error.type, error.message, error.stack_traceon errorsThe exception class, its message and the stack trace, as one field rather than continuation lines
log.loggeroptionalThe logger name

Other fields. Use the ECS field where one exists:

Otherwise put the field under your product's namespace (roller.post_id, wikantik.sections). Never invent a new top-level key, because it could collide with a future ECS field. Extra fields are welcome; they are simply not promised to exist across services.

The instance name is not in the line. The plane attaches service_instance (and host, deployment) to the log stream, exactly as it does for metrics (§1).

Where the JSON is parsed, and the one thing that must never happen

The plane does not parse log JSON at ingest today; the only loki.process block in jakemon extracts ollama metrics. The JSON is still useful now:

trace.id: the one field with cross-product semantics

A user hits a front end: Roller, a Wikantik page or a future product.

Agents use trace.id in Loki Query Language (LogQL) to reconstruct how one action moved through the stack. The trace is assembled by the plane, not by the product.

Propagation, in order of precedence:

  1. Honor W3C Trace Context if present. If the inbound request carries traceparent, take the trace-id from it. This is exactly what ECS and OpenTelemetry mean by trace.id, so adopting OpenTelemetry later is additive rather than a migration.
  2. Otherwise, honor an inbound X-Correlation-Id header.
  3. Otherwise, mint one. A 32-hex-character W3C trace-id is preferred, so that it can also travel as traceparent. Any opaque unique string is acceptable.

Send both headers onward: traceparent when you received or can construct one, and X-Correlation-Id always. A service that understands only one of them still participates.

Evolution rules (why this will not lock you in)

  1. The required set changes only by adding optional fields. A required field is never added later, a field is never renamed, and a meaning is never changed.
    • History: on 2026-10-12 the contract's own envelope (ts, level, msg, service, event, request_id, correlation_id) was replaced by the ECS fields above. This was a one-time move, made while no product emitted the old keys; see SA-6 in the decision log. The rule applies from this version on.
  2. Per-service fields under the product's namespace are unrestricted. If several services converge on the same field, it may be promoted to the contract as optional, using the ECS name if one exists. It is never demoted.
  3. Values, not fields, carry variation. Put a service-specific vocabulary in event.action values and namespaced fields, not in new top-level structure.
  4. Consumers ignore fields they do not recognize.

Getting a service onto ECS

Observed in Loki on 2026-10-12:

The exact changes for each repository are on SimpleAgilityTelemetryAdoption; the sequencing is on SimpleAgilityRoadmap. Until each product lands, the plane still captures its lines; what is lost is LogQL precision, not the logs.

3. Events are log lines, not a separate pipeline

Business and behavioral events ("page viewed", "email opened", "bounce received", "agent tool called", "opportunity snoozed") are emitted as structured log lines carrying event.action. Loki is the event store; LogQL aggregates them; the plane can ship an aggregated feed back to the product if the product needs to join on them. There is no event bus, topic, or product-side events table for analytics.

Two carve-outs, both important enough that the plane page states the general rule: a compliance audit log (tamper-evident, queryable by an auditor — Wikantik's /admin/audit) stays in the product, and so does any table that is a functional input to the product's own loop (retrieval_query_log feeding the agent_gap rule). Emitting the event line and keeping the table is correct, not duplication.

4. Health: one endpoint the plane can probe

Expose an unauthenticated liveness endpoint that returns 200 only when the service can do its job (dependencies reachable), and — if the product has one — a readiness endpoint the deploy tooling can wait on. Alert rules in the plane key on the probe plus the up metric; a product does not evaluate its own alerts.

The path is the product's choice; the contract needs one, not a specific spelling. Wikantik's is /api/health (plus /api/health/structural-index for subsystem readiness) — do not add a second /healthz alongside it. New products should prefer /healthz absent a reason.

Conformance

bin/simple-agility-conformance.sh --base-url http://localhost:8080 --prefix wikantik \
    --log-cmd 'docker logs --tail 200 wikantik'

It checks health, the metrics endpoint and its prefix, metric cardinality, and the log fields on a sample of real lines, and exits non-zero on any failure. Log checks report SKIP when no log source is given, so the first two parts can be used against any instance straight away. The log checks still test the pre-2026-10-12 envelope keys. Moving them to the ECS fields is a Wikantik-repository item on the roadmap.

Checklist for a new product

See also