Concepts

OpenTelemetry

How Yorker emits standard OTLP signals and correlates with your existing observability stack.

View as Markdown

Yorker is OTel-native. Every check emits standard OpenTelemetry signals -- metrics and traces -- using the OTLP HTTP JSON protocol. There is no proprietary telemetry format. If your backend speaks OTel, it works with Yorker.

Why OTel-native matters

Most synthetic monitoring tools store results in a proprietary system. When something breaks, you switch between your monitoring dashboard and your observability platform, manually correlating timestamps and URLs.

Yorker eliminates that context switch. Your synthetic check results land in the same backend as your application traces, logs, and metrics. A failing health check and the 500 error it triggered share the same trace ID.

Metrics emitted

Runner-direct OTLP emission (today: browser checks on any location, plus HTTP/MCP checks on private locations that have OTLP_ENDPOINT set) produces these metrics as OTLP gauge data points:

MetricTypeDescription
synthetics.http.response_timeGauge (ms)Total response time from request start to last byte received.
synthetics.check.successGauge (0 or 1)Whether the check passed all assertions.
synthetics.dns.lookup_durationGauge (ms)Time spent resolving DNS.
synthetics.tls.handshake_durationGauge (ms)Time spent on TLS handshake.
synthetics.browser.lcpGauge (ms)Largest Contentful Paint (browser checks only).
synthetics.browser.fcpGauge (ms)First Contentful Paint (browser checks only).
synthetics.browser.clsGauge (score)Cumulative Layout Shift (browser checks only).

These metrics follow OpenTelemetry semantic conventions for synthetic monitoring where they exist, and use the synthetics.* namespace for domain-specific signals.

Once you have configured an OTLP endpoint under Settings > Telemetry (OTLP), the control plane outbox path produces matching log events (synthetics.check.completed, synthetics.check.failed) for every check regardless of type or location. Those log event bodies carry the same response time, status, and timing breakdown as the runner-emitted metrics, so HTTP and MCP checks on Yorker-hosted locations still land observable data in your collector; you just query log events instead of gauges for those. Until you configure an endpoint, no outbox events are enqueued at all.

Resource attributes

Every metric, trace, and log event includes resource attributes that identify the check, location, and run:

AttributeExampleDescription
synthetics.check.idchk_abc123Unique check identifier.
synthetics.check.nameHomepageHuman-readable check name.
synthetics.check.typehttp, browser, or mcpCheck type.
synthetics.location.idloc_us_eastLocation identifier.
synthetics.location.nameUS East (Ashburn)Human-readable location name.
synthetics.location.typehosted or privateWhether the location is Yorker-hosted or a private location.
synthetics.run.idrun_xyz789Unique identifier for this specific execution.
url.fullhttps://example.comThe URL being monitored.
service.namesyntheticsService name used by both runner-direct emissions and control-plane outbox events.

These attributes let you filter, group, and alert on synthetic check data in your observability backend the same way you would with any other OTel-instrumented service.

Labels as resource attributes

Any labels attached to a check are emitted as additional resource attributes on every metric and trace. This lets you slice telemetry by your own dimensions (environment, service, team, criticality) without having to map check IDs back to metadata in your observability backend.

Label formatResource attribute
env:productionyorker.label.env="production"
service:paymentsyorker.label.service="payments"
critical (no colon)yorker.label.critical="true"

See Create a Monitor: Labels for how to attach labels.

Third-party network attribution

Browser checks emit four additional span attributes on the root synthetics.check.run span that describe the third-party network activity observed during the page load. These attributes are emitted whenever the browser executor produced a network summary, which is typical on every completed browser-check run. They are not emitted when the browser executor failed before the network summary was produced (for example, a browser launch failure), and they are not emitted on HTTP checks.

AttributeTypeUnitDescription
synthetics.third_party.countintrequestsNumber of network requests classified as third-party (registrable domain differs from the page's).
synthetics.third_party.total_bytesintbytesSum of decoded response sizes for all third-party requests in the page load.
synthetics.third_party.domainsstring[]--Unique third-party hostnames observed, capped at 50 entries. Example: ["cdn.tagmanager.net", "fonts.googleapis.com"].
synthetics.third_party.domains_truncatedboolean--true when the domains list was capped (the page had more than 50 unique third-party hostnames); false otherwise. Use this attribute for deterministic truncation detection. A page can legitimately have exactly 50 unique third-party domains, so domains.length == 50 alone is not a reliable truncation signal.

A request is classified as third-party when its registrable domain differs from the top-level page's. Subdomains of the same site are first-party (cdn.example.com is first-party to www.example.com). Tenants of a shared platform suffix are separate sites (other.vercel.app is third-party to mine.vercel.app). Hosts without a registrable domain, such as IP addresses, are compared by exact hostname.

When a page load has zero third-party requests all four attributes are still emitted (count=0, total_bytes=0, domains=[], domains_truncated=false) so queries can distinguish "confirmed zero third parties" from "no data" (the attributes are absent on HTTP checks).

Agent persona signals

HTTP monitors with agent personas switched on repeat their request as AI crawlers and compare each response with the normal one (the baseline). The core metrics and events for the check keep describing the baseline. Persona results are emitted as their own signals so they never skew a response-time chart or an availability query.

Event

Every persona request produces one synthetics.agent.persona.checked log event through the control-plane outbox, for hosted and private locations alike. Severity is INFO when the persona matched the baseline and WARN when it differed. It carries the check's resource attributes plus:

AttributeTypeDescription
synthetics.agent.personastringPersona ID, for example claude-user or gptbot.
synthetics.agent.operatorstringAnthropic, OpenAI or Perplexity.
synthetics.agent.purposestringuser, search or training.
synthetics.agent.divergencestringnone on a match, otherwise blocked, challenged, assertion_failed or error. Always present, so you can group by it.
synthetics.agent.divergence_reasonstringHuman-readable reason. Present only on a divergence.
http.response.status_codeintStatus code the persona received.
synthetics.response_time_msintResponse time of the persona request.
http.response.body.sizeintBytes received by the persona.
synthetics.agent.baseline.status_codeintStatus code the baseline request received.
synthetics.agent.baseline.body_sizeintBytes received by the baseline request.
synthetics.run.id, synthetics.location.id, synthetics.location.namestringSame values as the run's synthetics.check.completed event.

All events from one run share the run's trace ID.

Metrics and spans (runner-direct)

Where the runner emits OTLP directly (today, HTTP checks on private locations with OTLP_ENDPOINT set), a persona run also produces:

MetricTypeDescription
synthetics.agent.persona.matchGauge (0 or 1)1 when the persona matched the baseline.
synthetics.agent.persona.response_timeGauge (ms)Response time of the persona request.
synthetics.agent.persona.status_codeGaugeStatus code the persona received.
synthetics.agent.persona.body_sizeGauge (bytes)Bytes received by the persona.

Each metric has one data point per persona, carrying synthetics.agent.persona, synthetics.agent.operator, synthetics.agent.purpose and synthetics.agent.divergence on top of the usual resource attributes.

The check's trace gains one synthetics.agent.persona.request child span per persona under the synthetics.check.run root span, with user_agent.original, http.response.status_code and the persona attributes. A span for a persona that differed has ERROR status. The root span carries synthetics.agent.personas_tested (count) and synthetics.agent.personas_diverged (array of persona IDs).

Browser monitors running as an agent

A browser monitor with Run as set (agentPersona) carries synthetics.agent.persona, synthetics.agent.operator and synthetics.agent.purpose as resource attributes on every metric, trace and log it emits, alongside the usual check and location attributes. There is no comparison with a normal run in this case; the monitor's ordinary signals describe the run as that agent.

Agent view

A browser monitor with agent view on emits three more gauges per run:

MetricTypeDescription
synthetics.agent.view.no_js_text_ratioGauge (0 to 1)Share of the rendered text that was already in the HTML as served. Not emitted on a run where the HTML could not be read back.
synthetics.agent.view.controlsGaugeVisible, enabled buttons, links and form fields on the page the run ended on.
synthetics.agent.view.unnamed_controlsGaugeHow many of those have no accessible name.

What these signals mean

A persona request presents the crawler's user agent from a Yorker location. It is not signed by the operator and does not come from the operator's IP ranges, so it shows how your stack treats that user agent, not how a CDN that verifies bot identity treats the genuine crawler.

Agent readiness signals

HTTP monitors with agent readiness switched on probe their own site for robots.txt, llms.txt, a Markdown version of the page and discovery files.

Event

Each readiness run produces one synthetics.agent.readiness.checked log event through the control-plane outbox, for hosted and private locations alike. Severity is WARN when any probe fails or regressed, otherwise INFO. It carries the check's resource attributes plus:

AttributeTypeDescription
synthetics.agent.readiness.scoreintPassed probes as a percentage of those evaluated, 0 to 100.
synthetics.agent.readiness.passed_probesstring[]IDs of probes that passed.
synthetics.agent.readiness.failed_probesstring[]IDs of probes that were present but wrong.
synthetics.agent.readiness.missing_probesstring[]IDs of probes for things the site does not serve.
synthetics.agent.readiness.regressed_probesstring[]Probes that passed on the previous readiness run from this location and no longer do.
synthetics.agent.readiness.improved_probesstring[]Probes that did not pass on that previous run and now do.
synthetics.run.id, synthetics.location.id, synthetics.location.namestringSame values as the run's synthetics.check.completed event.

Changes are computed per location because some of what is probed can legitimately differ by region. The previous run is looked up within the last 7 days; with none in that window both change lists are empty. Probes that were not checked on either run are left out of the comparison.

Probe IDs are listed in the guide.

Metric and span attributes (runner-direct)

Where the runner emits OTLP directly (today, HTTP checks on private locations with OTLP_ENDPOINT set), a readiness run also produces a synthetics.agent.readiness.score gauge, and the check's synthetics.check.run root span carries synthetics.agent.readiness.score and synthetics.agent.readiness.failed_probes.

Cross-monitor correlation events

When two or more browser monitors in a team fail within a 5-minute window and each had failing requests to the same third-party dependency, Yorker emits one synthetics.correlation.detected OTLP log event per distinct correlation slice. A request is failing when it returned a 4xx or 5xx status or never got a response. Requests the browser cancelled or blocked itself do not count unless an error status had already come back. The event is co-occurrence-only: it proves requests to the same dependency failed in multiple failing runs, not that the dependency caused the failures.

The event fires for browser monitors only. HTTP and MCP checks do not populate the network-summary data the detection reads, so no correlation event is emitted for those check types. If a team has only HTTP or MCP checks, this event will never fire.

Attributes

AttributeTypeDescription
synthetics.correlation.affected_monitorsstring[]Check IDs identified in the scan as failing with a failing request to the shared dependency. Includes the triggering check. Sorted lexicographically. The array is capped at 50 entries to keep OTLP payloads under per-attribute size limits enforced by some backends; use affected_monitors_truncated to detect when the cap was hit. The underlying detection scan is itself bounded by 200 candidate rows per ingest.
synthetics.correlation.affected_monitors_truncatedbooleantrue when the emitted affected_monitors array was capped (more than 50 distinct check IDs were identified); false otherwise. When true, treat affected_count (not affected_monitors.length) as the authoritative cardinality.
synthetics.correlation.affected_countintNumber of distinct monitors identified in the scan. Always >= 2 for the event to fire. Authoritative cardinality, unaffected by the affected_monitors cap. The detection scan is bounded by 200 candidate rows per ingest, so under exceptionally high failure volume affected_count itself may be a lower bound on the full set.
synthetics.correlation.shared_dependencystringThe third-party domain that failed in the most distinct affected check IDs among domains that also failed in the triggering result.
event.namestringAlways synthetics.correlation.detected.

The standard resource attributes (synthetics.check.id, synthetics.check.name, synthetics.check.type, synthetics.location.id, synthetics.location.name, synthetics.run.id, plus any user labels) come from the triggering check that produced the event.

Threshold and dedup contract

The event fires when, for a triggering failing result with at least one failing third-party domain, two or more distinct check IDs (including the triggering one) within the 5-minute window have failed AND had a failing request to at least one of the triggering result's failing domains. A domain that both pages loaded successfully does not count.

The scan considers at most 50 of the triggering result's failing domains. Results from runners that predate failing-domain reporting (older private-location agents) take part using HTTP error statuses only; network-level failures such as refused connections are counted once the agent is upgraded.

The window is [triggering result's DB-stamped created_at - 5 min, triggering result's created_at], inclusive on both ends. Anchored to DB timestamps, not wall clock.

Dedup is keyed by the tuple (team, shared_dependency, affected_monitor_set). A sustained outage that keeps the same set of monitors failing produces one event per window for that combination. If the affected-monitor set changes (a new monitor joins the correlation, or a recovering monitor drops out) the combination key changes and a new event is emitted for the updated slice. This is the intended behaviour: the event represents a specific co-occurrence slice, and a different slice deserves its own event.

What this is and is not

The event is a co-occurrence signal. It indicates "these N monitors all failed within 5 minutes and requests to this third-party domain failed in each of them." It is not root-cause attribution: the third-party may itself be a victim of a deeper outage your monitors cannot observe, or the failures may be coincidental and the shared domain irrelevant. Investigate before paging the vendor.

If you want fine-grained correlation evidence, the alerting UI already provides it (per-incident hypothesis, scoped dimensions). The OTel event is for getting the same signal into your observability backend without polling Yorker's API.

Trace correlation

Yorker injects a W3C traceparent header into outbound requests during check execution. This is how it works:

  1. The runner generates a trace ID for the check execution.
  2. The traceparent header is added to the HTTP request (or injected into the browser's network requests for browser checks).
  3. Your backend application picks up the trace context via its own OTel instrumentation.
  4. The synthetic check span and your backend request span share the same distributed trace.

The result: when a check fails, you can click from the Yorker alert directly to the distributed trace in your observability backend. You see the synthetic request, the backend handler, the database query, and the error -- in one view.

Backend compatibility

Yorker works with any OTel-compatible backend. All Yorker telemetry is emitted as OTLP HTTP JSON, the most widely supported OTel transport. Tested backends include:

  • ClickStack (ClickHouse + HyperDX)
  • Grafana Cloud (Tempo + Mimir)
  • Datadog
  • Honeycomb
  • New Relic
  • Jaeger
  • Any OTLP-compatible collector (OpenTelemetry Collector, Alloy, Vector)

Emission model

Yorker has two OTel emitters, and they both target the same otlpEndpoint you configure on your team:

  • Runners emit OTLP metrics, traces, and logs directly to your collector, but only for browser checks today. Hosted HTTP and MCP runners do not emit OTLP from the runner process; private-location operators can enable runner-direct emission by setting OTLP_ENDPOINT/OTLP_API_KEY on their runner container when they start it.
  • The orchestrator drains an emission outbox that the control plane writes to, and ships every OTel log/span event Yorker generates to your collector, including synthetics.check.completed, synthetics.check.failed, synthetics.step.completed, alert state changes, SLO burn warnings, TLS certificate events, monitor/team insights, deployment markers, and maintenance-window events. The control plane enqueues; the orchestrator polls the outbox every ~10 seconds, runs SSRF guards, and POSTs.

The metrics catalogued above (synthetics.http.response_time, synthetics.check.success, and friends) are currently produced by the runner-direct path, which means you will see them for browser checks. For HTTP and MCP checks on hosted locations, the same information reaches your collector via the synthetics.check.completed / check.failed log events: they carry responseTimeMs, status, assertion results, timing breakdown, and the same resource attributes as the metrics, so dashboards and queries can key off either signal.

See the Telemetry flow section in Architecture for the full table of which check type and location combinations take which path.

Setup

To configure OTel emission for your team:

  1. Go to Settings > Telemetry (OTLP) in the Yorker dashboard.
  2. Enter your OTLP endpoint URL (e.g., https://otel-collector.example.com:4318).
  3. Enter your OTLP API key. The value is sent verbatim as the Authorization header on every OTLP request, so for a backend that expects a Bearer token enter the full Bearer <token>, not just the token. ClickStack expects the ingestion key on its own; see Connect ClickStack / HyperDX.
  4. If your backend needs a header other than Authorization (or more than one header), add them under Custom OTLP Headers. This lets you export directly to backends like Honeycomb (x-honeycomb-team), Datadog (dd-api-key), New Relic (api-key), or SigNoz Cloud (signoz-ingestion-key), without an OTel Collector in between. A custom header always wins over the OTLP API Key above if the names collide. Header values are encrypted and never shown again after save; use the row's value field to replace one, or remove the row to delete it.
  5. Click Test Connection. Yorker's control plane dispatches a test payload and reports success or failure.
  6. Save.

From this point, the control plane starts enqueueing events for the orchestrator to ship (for every check type and every location), and browser-check runners start including the endpoint (and any custom headers) in each execution payload for runner-direct metric/trace emission. Team-level OTLP credentials, including custom headers, are stored on the team, not per-check.

Private-location agents configure the same custom headers via environment variables instead of the dashboard: OTLP_API_KEY for the legacy Authorization value, plus the standard OTEL_EXPORTER_OTLP_HEADERS env var (comma-separated key=value pairs, values URL-encoded) for everything else. See Private Locations for the full syntax and precedence rules.