Skip to content

The example app ​

examples/hello is the one app every demo, test and page of DOTkit uses: a Vue page served by a small Node backend (node:http, no framework), instrumented with OpenTelemetry logs and traces. It is the workload of the contract's §2 example and the fixture app of the conformance tests (§7): when its nonce shows up in OpenObserve, the app, the wiring and the telemetry pipeline all worked.

To run or build it, see its README. This page describes what it does.

Its score.yaml ​

examples/hello/score.yaml is the contract §2 example byte for byte, except the image: line, which pins the published image by digest. That image is public and multi-arch (linux/amd64, linux/arm64): registry.gitlab.com/dotkit/dotkit/hello@sha256:7b8882a7…1535.

dotkit up --env local on this file is the intended demo path. It does not run yet: the CLI, generator, runner and primitives it needs are still being built.

Routes ​

Every route is matched on the path alone, whatever the method.

pathanswers
/healthz200 {"status":"ok"}, always
/readyz503 {"status":"starting"} until telemetry has started, then 200 {"status":"ready"}
/api/hello200 {"message":"hello from dotkit","nonce":"<n>"}; with telemetry on, one log record and one span
/ and every path outside /api200 with a file of the built front end, or the page itself (below)

The rest are JSON 404 {"error":"not found"}: any other path under /api (and /api itself), and a missing file with an extension (/assets/old.js), so a stale asset URL is never answered with HTML. A path without an extension that names no file (/orders/42) gets the page, for client-side routes.

/api is recognised as written. A percent-encoded form (/%61pi/nope, /api%2Fhello) is not an API path: it is served like any path outside /api (both examples get the page), never by a route, and nothing outside the built front end is reachable.

The page calls /api/hello when it loads and shows the answer.

The nonce ​

The conformance harness generates a one-time value per test run, passes it on the request, then searches OpenObserve for it: finding it proves this run's data travelled app -> OTLP -> OpenObserve, and stale data from an earlier run can never pass the test.

The nonce of a request is, in this order:

  1. the nonce query parameter (/api/hello?nonce=<n>),
  2. the DOTKIT_NONCE environment variable,
  3. a generated UUID, so a standalone run still emits something to search for.

An empty value counts as absent. The span (named hello) and the log record (body hello, nonce <n>) both carry the attribute dotkit.nonce = <n>; the record is emitted inside the span, so it carries the span's trace and span ids. OpenObserve stores the attribute as the field dotkit_nonce, in the default logs and traces streams.

Telemetry ​

Logs and traces go over OTLP/HTTP (JSON). The app reads two variables, once, at startup:

variablevalue
OTEL_EXPORTER_OTLP_ENDPOINTthe base URL; the exporters append /v1/logs and /v1/traces (OpenObserve: <url>/api/default)
OTEL_EXPORTER_OTLP_HEADERSAuthorization=Basic <base64 of user:password>, sent on every export

The headers value holds a credential. In score.yaml it is built from the sensitive output auth_token, so by the contract's taint rule (§2) the whole string reaches the container as a platform secret reference (ECS secrets[].valueFrom), never as a plain variable. The app reads it from its environment like any other variable and never logs or stores it.

OTEL_SERVICE_NAME overrides the service name (default hello). Metrics are off. Export failures, such as a wrong credential or an unreachable endpoint, are logged as warnings unless OTEL_LOG_LEVEL sets another level.

Readiness ​

modestartup log/readyz
OTEL_EXPORTER_OTLP_ENDPOINT settelemetry: otlp/http logs and traces503 until the OpenTelemetry SDK started, then 200
no endpointtelemetry: disabled200 from the start; nothing is exported

The SDK starts before the server listens and never waits for the endpoint, so a running container answers 200. /readyz says the SDK started, not that the endpoint is reachable: export failures show up in the log.

On SIGTERM or SIGINT the server stops accepting, flushes the batched logs and spans for at most 3 s (an endpoint that is gone does not delay the stop past that, and the drop is logged as telemetry: flush timed out), then exits 0.

The image ​

The build context is examples/hello alone; the README has the commands.

  • Multi-stage: the front end is built in one stage, production dependencies installed in another; no vite, vue or vitest in the image. Base: node:24-alpine, pinned by digest.
  • Runs as the unprivileged user node (uid 1000); nothing in /app is writable by it.
  • Listens on 3000; -e PORT=<n> overrides it.
  • HEALTHCHECK on /healthz every 10 s (timeout 2 s, start period 5 s, 3 retries).
  • node itself is PID 1 and handles SIGTERM (above), so docker stop never waits for the kill timeout.

Size of the published image, measured as the sum of its layers, unpacked (docker history, taken after the image has run once):

platformlayers, unpacked
linux/amd64195,575,808 bytes
linux/arm64191,152,128 bytes

The command, per platform:

sh
docker history --platform linux/<arch> --format '{{.Size}}' --human=false <image> \
  | awk '{s+=$1} END {print s}'

Compressed in the registry it is about 64 MB. Do not read the size from docker images: on a containerd image store that column adds the unpacked snapshot to the stored content, and for a pulled multi-arch image counts every platform pulled, so it shows a number well over 200 MB. On a containerd store, docker history of an image for another platform reports only the base until that image has run once.

First /healthz answer after docker run: under 1.3 s on arm64, measured natively. It has not been measured on a native amd64 host yet; under emulation it is slower, which measures the emulator, not the image.

Released under the MIT License