Reference and explanationEarlier operations docs

Datadog monitoring baseline

The signals the Datadog Agent collects, the initial monitor thresholds, the DB check queries, and how to apply and verify the definitions

This document is the minimum monitoring baseline to check on a server deployment. The metrics the platform emits on a Kubernetes (Helm) install are in Platform metrics. The app does not include the Datadog SDK. It exports session traces over standard OTLP only when you configure it (off by default, no particular collector required); the keys are in the API server operations doc. The Datadog Agent described here does not handle traces; it watches logs, /readyz, the database and the host. The Datadog Agent collects Docker's JSON stdout logs, /readyz, read-only PostgreSQL queries and host metrics. The definitions are version-controlled only in infra/datadog/definitions, and no tool or CI in this repository calls the Datadog API.

Every threshold is an initial value to tune during the first 7 days after the server deployment. Do not treat a monitor tagged threshold_stage:first-week-tuning as a long-term baseline without the first week's data. Gaps already filled are marked None, and items outside that scope whose priority will be set again after the server deployment are marked Decide after deployment.

Startup and secret handling

The Agent is created only by the optional infra/compose.datadog.yml. The default stack, which does not name this file, gets no Agent, mount, label or required environment variable at all. The Agent image is pinned to gcr.io/datadoghq/agent:7.83.3.

Do not put DD_API_KEY or the PostgreSQL password for Datadog in the repository or in shell history. On the server, the operator creates /etc/werft/datadog.env and checks that its mode is 600. Change it with DATADOG_ENV_FILE only when you must use a different absolute path. The values are example placeholders, so do not copy real values into documents, tickets or logs.

DD_API_KEY=<operator-supplied>
DD_SITE=datadoghq.com
DD_ENV=alpha
DD_VERSION=<release-sha>
DD_POSTGRES_PASSWORD=<read-only-role-password>
sudo chown root:root /etc/werft/datadog.env
sudo chmod 600 /etc/werft/datadog.env
stat -c '%a %U:%G' /etc/werft/datadog.env

The Agent service reads this absolute path only as an env_file. The compose YAML has no interpolation of the name or value of DD_API_KEY, so an ambient DD_API_KEY cannot bypass it. DD_ENV and DD_VERSION are read from the same file as compose's --env-file and used for the unified tags of the product containers.

The PostgreSQL role is allowed only SELECT on queue_messages, sessions, turns, attempts and provider_usage, and its default transaction is fixed to read-only. The server operator runs the commands below once on an admin connection.

CREATE ROLE datadog LOGIN PASSWORD :'datadog_password';
ALTER ROLE datadog SET default_transaction_read_only = on;
GRANT CONNECT ON DATABASE sessions TO datadog;
GRANT USAGE ON SCHEMA public TO datadog;
GRANT SELECT ON TABLE queue_messages, sessions, turns, attempts, provider_usage TO datadog;

Keep the layers of the existing deploy command as they are, add only this overlay as the last -f, and give the file above as one more --env-file. Below is the example on the local stack's entry file infra/compose.yml. On a server install, list that install's own layers in its place.

docker compose \
  --env-file /etc/werft/datadog.env \
  -f infra/compose.yml \
  -f infra/compose.datadog.yml \
  --profile apps up -d

This form was checked as far as docker compose config with the repository's compose files. Adding the overlay adds one service, datadog-agent, to the service list and puts Datadog labels on api, scheduler, reconciler, egress-proxy and postgres. The service list was checked by rendering; the labels are read from infra/compose.datadog.yml and were not checked in the rendered output. Without DD_ENV or DD_VERSION, the render is refused. Running up on a real server and the Agent reporting to Datadog were not checked (NOT VERIFIED).

The Agent mounts the Docker socket and the container log directory read-only and reads the host PID and cgroup namespaces. With DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true, it also collects dynamic workers. Compose services get the service, env and version labels. For dynamic workers, service is derived from the container short-image, env comes from the Agent's global DD_ENV, and version from the global DD_TAGS. session_id and turn_id are kept only as log attributes for investigation and are not made into metric tags or facets.

Signals and initial monitors

The original record of the structured logger is timestamp, level, the machine-readable event, the human-readable message, the optional fields, and context (session_id and so on). The log pipeline parses the JSON and uses message as a fallback only when an older record has no event. Detailed values are under fields.*. The metric names in the queries are the names made by log-metrics.json and the PostgreSQL custom queries.

RiskCurrent signal (log event and fields, DB query, readyz)Datadog monitor (query, threshold, window)Runbook on alertInstrumentation gap
interrupt effect exceeds 5 secondsfields.operation:interrupt, effect_ms, outcome, reason_code, control_id of control.result → werft.control.effect.duration_ms. For per-stage diagnosis, use worker.turn.interrupt.stage with the same control_idp95 avg(last_5m):p95:werft.control.effect.duration_ms{operation:interrupt} > 5000; unknown result > 0/5mTurns whose result is unknown after an interruptNone
terminate and control acceptance latencyapi.request.completed for every completed /v1 request (route_template, method, status, duration_ms, request_id). A terminate 202 is made into werft.terminate.acceptance.duration_ms and correlated with the control transaction stages of the same request_idp95 avg(last_5m):p95:werft.terminate.acceptance.duration_ms{*} > 200terminate instrumentation in API settingsNone
session creation p95route_template:/v1/sessions, method:POST, duration_ms of api.request.completed → werft.session.create.duration_msSet the threshold after observing p95 on the dashboard in the first week of deploymentAPI settingsNone. Choosing the threshold is separate from an instrumentation gap
checkpoint failureworker.checkpoint.failed (fields.stage, fields.reason, fields.revision) → werft.checkpoint.failedsum(last_5m):sum:werft.checkpoint.failed{*}.as_count() > 0Sessions whose checkpoint restore keeps failingNone
checkpoint pending reason and agesessions.checkpoint_pending_reason; custom query werft.checkpoint.pending_age_secondsmax(last_5m):max:werft.checkpoint.pending_age_seconds{*} > 300Sessions whose checkpoint restore keeps failingsessions.updated_at is a conservative proxy for the start of pending; the exact start time is Decide after deployment
capture latency, PUT count, bytes and manifest sizeFor each success, rejection and failure, one worker.checkpoint.result records duration_ms, durations_ms, put_count, uploaded_bytes, manifest_bytes, reused_count, conflict_count, outcome and reason_code. The bundle and untracked sizes of worker.checkpoint.published and worker.checkpoint.late are also keptcapture p95 > 20,000 ms/5m; late count > 0/5m; bundle and untracked > 214,748,364 bytes/15m; manifest > 6,710,886 bytes/15mSessions whose checkpoint restore keeps failingNone
S3 5xx and stallThe outcome, reason_code and per-stage durations_ms of the checkpoint result tell capture results apart, but the S3 response itself is not structuredNo monitor defined, because there is no reliable S3 status queryChoosing an object storeS3 operation/status/duration is Decide after deployment
repeated worker launch/exitLaunching execution failed; intent kept for retry, Execution resource exited right after launch, Launch attempt failed; retrying after a backoff → werft.launch.failed; Execution exited; resource reclaimed, Claimed execution resource vanished; binding released → werft.execution.exitedlaunch: sum(last_10m):sum:werft.launch.failed{*}.as_count() > 3; exit: sum(last_10m):sum:werft.execution.exited{*}.as_count() > 5Sessions that keep dying at startupNone
convergence after a storage outageExecution launched, the launch/exit events above, fields.requeued_count and fields.blocked_count of Orphan session reconciliation completedThe repeated launch/exit monitors detect the symptomreconciler and schedulerThe time from the start of the outage until every session converges, and the number of sessions that did not converge, are Decide after deployment
lease lostExpired lease reconciliation completed (fields.fenced_count, fields.ended_count); custom query werft.lease.stale_countmax(last_5m):max:werft.lease.stale_count{*} > 0reconciler and schedulerNone
queue oldest ageMinimum of unclaimed, visible queue_messages.created_at → werft.queue.oldest_age_secondsmax(last_5m):max:werft.queue.oldest_age_seconds{*} > 30reconciler and schedulerNone
readyz availabilityAgent HTTP check werft-readyz → http.can_connectFailure in the last 3 checks, 5 minutes of no dataAPI settingsNone
provider 429 and 5xxThe proxy records only upstream connect failures and does not record the provider response statusNo monitor defined, because there is no reliable status queryprovider and egress settingsProvider status and retry results are Decide after deployment
provider cost and fallback pricing1-hour totals of provider_usage → werft.provider.cost_usd_1h, werft.provider.fallback_pricing_count_1h, werft.provider.unmetered_tokens_1hcost > 20 USD/1h; fallback count > 0/1hInstallation limitsNone. The cost threshold must be tuned to the installation's budget in the first week
disk, inode and XFS quotaAgent host system.disk.in_use, system.fs.inodes.in_use; xfs_quota currently checked by handhost disk and inode > 0.8 for 10mLimits and reclaiming of worker workspacesA per-workspace XFS project quota usage metric is Decide after deployment
growth of legal holds and orphansNo checkpoint inventory metric todayNo monitor definedVersions and holds of checkpoint objectsDecide monitoring again after the inventory metric is implemented
backup failureThe exit of scripts/backup.sh and the operations history do not reach Docker JSON stdoutNo monitor definedBackupStructuring backup run results is Decide after deployment

DB check query contract

The custom_queries in infra/datadog/conf.d/postgres.d/conf.yaml are the source of truth. Every query is a single SELECT and does not change any table.

  • queue oldest age: compares the oldest created_at of messages not yet claimed and already visible with the DB clock.
  • checkpoint pending age: uses the oldest updated_at among sessions with checkpoint_pending_reason IS NOT NULL. Updates to the same session unrelated to the checkpoint can move this time, so it is a lower-bound proxy, not an exact SLI.
  • outcome_unknown: the number of turns.outcome_unknown = true rows created in the last 5 minutes.
  • stale lease: the number of attempts that are not exited or lost with lease_expires_at < clock_timestamp().
  • provider cost: the total cost_usd over the last hour and the number of priced_by = 'fallback' rows. werft.provider.unmetered_tokens_1h is the token total (input, output, cache write, cache read) of priced_by = 'unmetered' rows over the same hour. A billing: none profile leaves cost at 0, so these token counts are the only place where a wrongly declared paid upstream shows up. No monitor is defined, because the normal value depends on the installation's local model usage.

No query returns session_id. The group-by in log-metrics.json also uses only service, env and low-cardinality states. session_id is used only to narrow down the original logs or the DB during an incident.

Applying and verifying the definitions

The definitions are not applied automatically. After reviewing the JSON, the operator applies the requests entries one by one to the endpoint of each file. log-facets.json is the list of facets to create in the UI and the high-cardinality attributes not to create. dashboard.json is the dashboard create body. Do not create monitors, dashboards or log pipelines that are already applied again; edit the names, queries and filters of the existing ones to match the new definitions. Creating them again leaves two copies of the same definition.

On the server deployment, check the following.

  1. When the default stack is rendered and started without the overlay, the service list and settings are the same as before.
  2. docker compose config with the overlay refuses a missing DD_ENV or DD_VERSION, the Agent's Docker socket mount is ro, and it reads only /etc/werft/datadog.env.
  3. In the Agent status, the Docker logs, http_check:werft-readyz and the PostgreSQL custom queries all succeed.
  4. On the server, make /readyz fail briefly and check that the service check changes and recovers. Local verification evidence does not replace an actual long-running stack run or monitor notification delivery.

On this page