Webhook icon
Schedule icon
WorkingDirectory icon
Commands icon
Docker icon
Request icon
Script icon
If icon
Loop icon
Log icon
Create icon
SlackIncomingWebhook icon

Find Slow Jaeger Services and Escalate the Latency Digest

Pulls recent Jaeger traces, computes per service p50 and p99 latency, and escalates services over the p99 budget to GitHub and Slack.

Categories
Infrastructure

A latency spike that only lives in the tracing UI is a latency spike nobody owns. This flow gives the trace window a verdict and an escalation path. A keyed webhook triggers an on-demand digest and a disabled hourly schedule holds the cadence. The flow queries the Jaeger trace search API, keeps a bundled six span fixture when no collector answers, and turns span durations into per service p50 and p99 figures. Services over the p99 budget are noted one per line, filed as a GitHub issue and posted to Slack, while a clean window is logged and the verdict, slow services, worst p99 and trace count ship as typed outputs.

How it works

  1. on_demand_digest (io.kestra.plugin.core.trigger.Webhook) exposes a keyed URL for post-deploy or post-incident checks, and hourly_digest (io.kestra.plugin.core.trigger.Schedule, cron 0 * * * *) ships disabled: true so nothing runs until the cadence is switched on.
  2. trace_workspace (io.kestra.plugin.core.flow.WorkingDirectory) opens one shared filesystem so the fixture, the API response and the parser all see the same files.
  3. write_traces_fixture (io.kestra.plugin.scripts.shell.Commands on the io.kestra.plugin.scripts.runner.docker.Docker runner, image alpine:3.20) writes traces.json with a heredoc in the Jaeger /api/traces shape. The fixture holds three traces and six spans across checkout-api and search-api, four spans stay between 29ms and 55ms while two spans land at 3200ms and 2100ms, so the offline p99 is 3200.0ms against the 1000ms default budget. entryPoint: [] clears the image entrypoint so the shell script runs instead of a server command.
  4. fetch_traces (io.kestra.plugin.core.http.Request) GETs {{ inputs.jaeger_base_url }}/api/traces?service=&limit={{ inputs.trace_limit }} with Accept: application/json, and options.allowFailed lets a 4xx, 5xx or timeout answer through so the digest continues on the fixture instead of dying.
  5. analyze_traces (io.kestra.plugin.scripts.python.Script on python:3.12-slim) prefers the data array from the Jaeger response and falls back to traces.json when the body is missing or unparseable. Every span duration is converted from Jaeger microseconds to milliseconds, grouped by process.serviceName, and scored with nearest rank p50 and p99. A service is slow when p99_ms > p99_threshold_ms. The verdict, the slow service rows, the worst p99 and the trace count are printed as a single ::{"outputs": ...}:: line.
  6. slow_span_gate (io.kestra.plugin.core.flow.If) branches on slow_services | length > 0. Above zero, note_slow_services (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) writes one core.log.Log note per slow service, open_slow_span_issue (io.kestra.plugin.github.issues.Create) files a labeled issue listing every slow service with its p50 and p99, and alert_slow_spans posts the counts to Slack. At zero, log_within_budget records the quiet window.
  7. The flow level errors handler (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) reports any failure with the execution id, because a digest that dies silently looks exactly like a healthy service.

What you get

  • A latency verdict per run, SLOW_SPANS, WITHIN_BUDGET or NO_TRACE_DATA, with the worst service p99 as a float any downstream flow can gate on.
  • Per service p50 and p99 math instead of a single average, so one slow endpoint is not averaged away by its fast siblings.
  • Escalation where engineers already work, one log note per slow service, a labeled GitHub issue carrying the full slow list, and a Slack ping with the counts, plus a quiet log when everything is inside the budget.
  • Offline determinism, the bundled fixture always reports checkout-api at a 3200.0ms p99, so the first run proves the escalation path before a real collector is wired up.

Who it's for

  • SRE and platform teams who own latency objectives and want a p99 digest that escalates itself instead of relying on someone opening Jaeger.
  • On-call engineers who need slow services to arrive as a channel message and a backlog issue with the numbers attached.
  • Backend developers tracking whether a deploy pushed a service past its p99 budget.

Why orchestrate this with Kestra

A Jaeger bookmark and a dashboard do not create work when latency slips. Kestra wraps the check in the pieces a dashboard cannot provide, a keyed webhook for post-deploy runs, a disabled hourly schedule as the safety net, an HTTP step that degrades to a fixture instead of failing, a branch that separates slow from healthy, a loop that leaves one log line per offender, and routing into GitHub and Slack from one version controlled file. The same file carries the budget, the escalation policy and the execution history that proves the digest actually ran.

Prerequisites

  • A worker with Docker access, the fixture is written in alpine:3.20 and the analysis runs in python:3.12-slim.
  • A Jaeger query API reachable at the jaeger_base_url input, or rely on the bundled fixture while you trial the flow.
  • A GitHub token that can open issues in the repository named on open_slow_span_issue (classic PAT scope repo, or fine-grained Issues: write).
  • A Slack incoming webhook for the slow span alert and for failure alerts.

Secrets

  • JAEGER_DIGEST_WEBHOOK_KEY: key segment guarding the on demand webhook URL, anyone holding it can start a digest.
  • GITHUB_TOKEN: token used to open the slow service issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL for the slow span alert and for failure alerts.

Quick start

  1. Add the three secrets to your Kestra namespace.
  2. Run the flow once with defaults and confirm verdict reads SLOW_SPANS, slow_services lists checkout-api at 3200.0ms and search-api at 2100.0ms, p99_ms reads 3200.0 and traces_scanned reads 3.
  3. Raise p99_threshold_ms to 5000 and re-run to watch the gate flip to log_within_budget with verdict WITHIN_BUDGET.
  4. Trigger on_demand_digest at /api/v1/{tenant}/executions/webhook/{namespace}/jaeger-slow-span-digest/{JAEGER_DIGEST_WEBHOOK_KEY}, then enable hourly_digest once the worker can reach Jaeger.
  5. Point open_slow_span_issue.repository at your own runbooks repository before the first slow run.

How to extend

  • Scope the query to one service by adding the service name to the fetch_traces uri when service= should not stay empty.
  • Store every digest row with io.kestra.plugin.jdbc.postgresql.Queries so p99 drift is chartable over quarters.
  • Fan the digest out per namespace with io.kestra.plugin.core.flow.Subflow when several Jaeger deployments must be checked each hour.
  • Add a p50 guard beside the p99 guard inside analyze_traces when steady state creep matters as much as tail latency.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.