OTel Instrumentation Coverage Auditor
Pricing
from $750.00 / 1,000 trace set auditeds
OTel Instrumentation Coverage Auditor
Find what isn't instrumented: dark dependencies, broken context propagation, uninstrumented time, missing resource metadata and log correlation gaps across a fleet of OpenTelemetry traces, with a prioritized fix plan.
Pricing
from $750.00 / 1,000 trace set auditeds
Rating
0.0
(0)
Developer
Justin Hagerty
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
11 days ago
Last modified
Categories
Share
Give it a batch of OpenTelemetry traces (and, optionally, logs and metrics). It tells you what isn't instrumented: services that never emit spans, hops where trace context gets lost, time inside requests that no span accounts for, missing resource metadata, and logs that can't be joined to traces. You also get a coverage score per service and a ranked list of fixes.
Most trace tools help you work out why one request was slow. This Actor answers a different question: where is the trace data itself incomplete? A trace can look healthy and still be missing half of what happened. A downstream service may have no SDK. A gateway may strip traceparent. One pod may run without the JDBC instrumentation. A Kafka consumer may start a new trace for every message. None of these raise errors. They leave gaps in the data that you only notice during an incident.
Coverage is a property of the whole fleet, so the Actor analyses many traces together. It runs 55 rules (COV-*) across topology, propagation, gaps, metadata, scopes, span semantics and logs/metrics correlation. It then builds a dependency graph (JSON plus Mermaid), scores every service from 0 to 100, and turns the findings into an instrumentation improvement plan. Each action in the plan has an effort level, an impact level and hints for the languages your services actually run.
Results are suspected gaps, not proof. A missing span can't prove that an operation happened. Sampling, export failures or an incomplete sample can look the same as missing instrumentation. Every finding is phrased as suspected and carries a
confidencevalue. Verify before acting.
Why use it
- Find dark dependencies. These are internal services your code calls that never emit a span. The Actor separates them from databases, caches, brokers and SaaS APIs (
db.system,messaging.system, known SaaS hostnames such asapi.stripe.com), which aren't expected to emit spans. - Find broken context propagation. When a
CLIENTspan has noSERVERchild in its trace, the Actor looks for a rootSERVERspan of the target service that started in another trace inside the client's time window, with the same route and method. That pins down the exact hop where context is lost. - Measure uninstrumented time. Child spans are merged as an interval union, so concurrent children aren't double-counted. Handlers whose children cover less than a threshold share of their duration are reported. So are handlers that make no calls at all in a service that is known to call databases or other services elsewhere.
- Catch inconsistent rollouts. These include routes where only some requests show DB spans at similar latency, and pods that lack a scope their siblings have (for example
io.opentelemetry.jdbc). The evidence is broken down byservice.version. - Fix metadata that breaks every dashboard. These include
unknown_service:*, missingservice.version/deployment.environment.name/service.instance.id, inconsistent infra attributes, outdated or mixed SDK versions, and name collisions. - Join logs and metrics to traces. The Actor finds log records without trace context, trace IDs in logs that aren't in the trace set, and services that appear only in logs or metrics. It also finds services whose client metrics show database calls while no DB spans exist.
- Catch coverage regressions. Pass a previous run as a
baseline(or its dataset ID) to get per-service score deltas, services that disappeared and findings that are new since then. Plan actions that touch regressed services are ranked higher. - Works with service meshes. Envoy / Istio / Linkerd sidecar spans are recognised and collapsed, so proxies don't show up as badly instrumented "services" and calls through a mesh still pair up.
- Know what to do next. The improvement plan is ordered by
impact × severity × confidence / effort. Each action includes concrete steps such asOTEL_PROPAGATORS=tracecontext,baggage, enabling Java agent JDBC, addingopentelemetry-instrumentation-psycopg, settingOTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES, or adding span links for messaging.
Who it's for
- Platform and observability teams rolling out OpenTelemetry across many services, who need to know which services and hops still need work.
- SREs after an incident where a trace "stopped" somewhere, who want to find every place that can happen.
- Consultancies running observability-maturity assessments that need a repeatable, explainable report and score.
- CI pipelines that export traces from a staging test run and fail the build when coverage regresses (
failOnSeverity, SARIF output).
How it works
- Parse. Every input is normalised to one span model (lowercase hex IDs, nanosecond timestamps, resource, scope, kind, status, links). The formats are OTLP/JSON (hex or base64 IDs, legacy
instrumentationLibrarySpans, Tempobatches), Jaeger JSON, Zipkin v2 (includingsharedclient/server spans) and a simplified span list. Formats can be mixed in one trace set. Large exports can be read from an Apify dataset or key-value store record. Service-mesh proxy spans (Envoy / Istio / Linkerd, recognised bycomponent=proxy,upstream_cluster,istio.*,node_id=sidecar~…and similar) are collapsed: their children are re-parented to the calling application span. - Resolve peers. Each outbound span (
CLIENT/PRODUCER, or any span withdb.system) is mapped to what it calls. Sources are tried in this order: the paired child span in the same trace, thendb.system/messaging.system, thenpeer.service, then a host index learned from paired calls andSERVERspan attributes, then the DNS label (inventory.shop.svc.cluster.local→inventory), then your expected services. Anything left over is classified by host: internal (no dot,*.svc.cluster.local,*.internal, private IPs and similar), SaaS (bundled suffix list) or external. - Derive. The Actor builds the dependency graph and dark dependencies. It matches orphans, roots and propagation breaks (one-to-one, closest start first, compatible route and method). It also computes gap groups (interval union per span), per-route downstream coverage by type (
db/remote/messaging), per-instance scope sets, propagator hints, correlation IDs shared across traces, and the logs/metrics joins. - Run the rules. All 55 rules are pure functions. They only look at the telemetry, never at the network, so the same input always gives the same output.
- Score and plan. Each service gets four sub-scores and a combined score (see Scoring). The fleet gets one score, and findings are grouped into actions in the improvement plan.
Rule categories
| Category | Rules | Examples |
|---|---|---|
| Topology & discovery | COV-TOPO-001…008 | Uninstrumented service named by peer.service (high). Internal host never seen as a service. Expected service never observed. Expected/unexpected edges from your service map. Outbound spans without any peer attributes. Isolated services. |
| Propagation | COV-PROP-001…012 | Orphan spans. Traces without a root. Matched propagation breaks (timing plus route/method, or a parent ID found in another trace). Called services starting new traces. Mixed propagators (b3 / tracecontext / jaeger / xray / datadog headers, 64-bit IDs). Messaging consumers without parent or link. Traces that start with a CLIENT span. Same request or message ID in several unlinked traces. SERVER → SERVER with no client span. Partial traces. Inconsistent tracestate sampling. |
| Gaps | COV-GAP-001…006 | Children cover less than gapRatio of a span. Opaque long handlers. Inconsistent downstream coverage per route. Instances instrumented differently. No INTERNAL spans in long handlers. Child spans that outlive their parent. |
| Resource metadata | COV-META-001…010 | unknown_service:*, missing service.version / environment / service.instance.id, inconsistent host.* / k8s.* / cloud.* attributes, missing telemetry.sdk.*, outdated SDK (approximate table), mixed SDK versions, one name used by several languages, deprecated deployment.environment. |
| Instrumentation scopes | COV-SCOPE-001…003 | Spans without a scope name. Services with manual spans only (no auto-instrumentation library). The same library at several versions. |
| Span semantics | COV-KIND-001…003, COV-STATUS-001…002 | Spans carrying HTTP/RPC attributes in a service that emits no SERVER/CLIENT spans. SERVER spans without HTTP/RPC/messaging attributes. DB spans that aren't CLIENT. Errors without ERROR status. HTTP server spans without http.route. |
| Logs/metrics correlation | COV-CORR-001…009 | Logs without trace context. Trace IDs only in MDC attributes. Log trace IDs missing from the trace set. Services in logs or metrics only. Server or DB client metrics without matching spans. Resource attributes that differ between signals. Log span IDs not in the trace. |
| Data quality | COV-DATA-001…002 | Small samples, duplicate span IDs. |
Rule IDs are stable and never reused. See RULES.md for the full catalogue, with severity, rationale, recommendations and references.
Scoring
Each finding is attributed to the services in evidence.services and counted in one of four dimensions:
| Dimension | Categories | Weight |
|---|---|---|
propagation | topology, propagation | 35% |
depth | gaps, span semantics | 30% |
metadata | metadata, scopes | 20% |
correlation | logs/metrics correlation | 15% (dropped and the rest re-normalised when no logs or metrics were supplied) |
A dimension starts at 100 and loses points × confidence per finding: high = 30, medium = 15, low = 6, info = 0. Repeated findings of the same severity decay by ×0.6 each, so twenty small issues can't outweigh one large one. The service score is the weighted mean of its dimensions. The fleet coverage score (the report's score) is the span-weighted mean of the service scores, minus fleet-wide findings such as an expected service that never appeared. risk_level is derived from the score in the same way as in all our Actors. Scores are deterministic, so you can track them over time and gate CI on them.
How to use
In Apify Console
- Paste your traces into Traces. Start from the prefilled demo, or paste an export from your Collector's
fileexporter, the Jaeger UI ("Download JSON") or the Zipkin API. - Optionally list your Expected services, or provide an Expected service map.
- Optionally add Logs and/or Metrics (OTLP/JSON) to run the correlation checks.
- Click Start. The Output tab has an Overview view, a Findings view (one row per finding), a Services view, a Topology & gaps view and an Improvement plan view. The key-value store holds
OUTPUT,REPORT.mdandREPORT.sarif.
Via the API
jq -n --slurpfile t traces.json --slurpfile l logs.json \'{traceSetName: "shop-prod", traces: $t, logs: $l, expectedServices: ["frontend","checkout","inventory"]}' |curl -s -X POST -H "Content-Type: application/json" -d @- \"https://api.apify.com/v2/acts/michiganpublictech~otel-coverage-auditor/run-sync-get-dataset-items?token=$APIFY_TOKEN" |jq '.[0] | {score, summary, plan: [.data.improvement_plan[] | {rank, title, services}]}'
Python (apify-client)
import jsonfrom apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("michiganpublictech/otel-coverage-auditor").call(run_input={"traceSetName": "staging-load-test","traces": [json.load(open("otlp-traces.json"))], # wrap one export document in a list"serviceMap": {"frontend": ["checkout", "cart"], "checkout": ["inventory", "postgresql"]},"failOnSeverity": "high",})report = next(client.dataset(run["defaultDatasetId"]).iterate_items())print("fleet coverage", report["score"])for s in report["data"]["services"]:print(f"{s['service']:20} {s['score']:3} {s['breakdown']}")print(report["data"]["dependency_graph"]["mermaid"])
Getting traces out of your stack
- OpenTelemetry Collector: add a
fileexporter (format: json) to the traces pipeline for 10–15 minutes. The output is JSON Lines ofExportTraceServiceRequest, and you can pass it as a string insidetraces("traces": ["<file contents>"]). - Jaeger: use the UI's Download JSON or
GET /api/traces?service=…&limit=…. The{"data": [...]}document works as-is. - Zipkin:
GET /api/v2/traces?limit=…returns a list of span lists. Pass it as-is. - Anything else: map spans to the simplified format below.
Large exports (Apify storage)
The Actor input is limited to a few MB. For bigger exports, store the traces in Apify storage and reference them:
- Dataset (
traceDatasetId): one span (simplified or Zipkin v2) or one export document (OTLP/JSON, Jaeger JSON) per item. Up to 200,000 items are read. - Key-value store record (
traceRecordKey, plustraceKeyValueStoreIdunless the record is in the run's default store): one OTLP/JSON or Jaeger document, a span list, or JSON Lines text straight from the Collectorfileexporter.
Storage input is added to the traces trace set. Inside traceSets, an entry can use datasetId, or recordKey plus optional keyValueStoreId, instead of inline traces. A dataset or record that can't be read becomes a free failed item, and the run continues. Storages are opened by ID (never by name, which would create a missing storage). traceDatasetId, traceKeyValueStoreId and baselineDatasetId use Apify's storage picker and request read access only, so they also work when the Actor runs with limited permissions. Storage IDs inside traceSets entries are opened with the run's token and must be accessible to it. Each set is still capped at 100,000 spans.
from apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")store = client.key_value_stores().get_or_create(name="otel-exports")client.key_value_store(store["id"]).set_record("shop-prod",open("traces.jsonl").read(),content_type="text/plain", # Collector file exporter output)run = client.actor("michiganpublictech/otel-coverage-auditor").call(run_input={"traceSetName": "shop-prod", "traceRecordKey": "shop-prod", "traceKeyValueStoreId": store["id"]})
Input
| Field | Type | Description |
|---|---|---|
traces | array | The trace set to audit. Either a list of spans (simplified or Zipkin v2) or a list of documents (OTLP/JSON ExportTraceServiceRequest, Jaeger JSON). Items may also be JSON / JSON Lines strings. |
traceSetName | string | Report target name for traces (default traces). |
traceSets | array | More independent sets in one run: [{name, traces, logs?, metrics?, expectedServices?, serviceMap?}]. Each set gets its own report and is billed separately. An entry can read its traces from storage with datasetId, or recordKey + optional keyValueStoreId. |
traceDatasetId | string | ID of an Apify dataset holding spans or export documents (see Large exports). |
traceRecordKey | string | Key of a key-value store record holding an export (JSON, or JSON Lines text). |
traceKeyValueStoreId | string | ID of the key-value store holding traceRecordKey (default: the run's default store). |
expectedServices | array | service.name values you expect. Inside traceSets this field can also be a {service: [deps]} map. |
serviceMap | object | {service: [dependencies]}. Keys become expected services. Edges that never appear are reported. Dependencies may be services, db.system / messaging.system names or host labels. |
logs | array | OTLP/JSON ExportLogsServiceRequest documents or simplified records {service, traceId, spanId, time}. Log bodies are never copied to the output. |
metrics | array | OTLP/JSON ExportMetricsServiceRequest documents or {service, metrics: [names], resource?}. Only resources and metric names are used. |
minGapMs | integer | Minimum uninstrumented time worth reporting (default 100 ms). |
gapRatio | number | Report a span when its children's interval union covers less than this share of it (default 0.5). |
clockSkewMs | integer | Clock skew allowed when matching a call to a server span that started a new trace (default 100 ms, 0–60,000). Raise it if your hosts' clocks drift. |
baseline | array | A previous run's trace-set dataset items, or compact summaries [{traceSet?, services: {name: score}, fleetScore?}]. Enables data.regressions. |
baselineDatasetId | string | ID of a previous run's default dataset to use as the baseline. If it can't be read, the run continues without it (with a note). |
perServiceItems | boolean | Also push one free dataset item per service (default false). |
failOnSeverity | string | CI gate: none (default) / critical / high / medium / low. |
disabledRules | array | Rule IDs to skip (plans with custom rules). |
licenseKey | string (secret) | Optional enterprise or prepaid key. |
Input formats with examples
Simplified span list. Times are epoch milliseconds or ISO-8601. Use durationMs or endTime. kind defaults to INTERNAL. Common aliases such as traceID and serviceName are accepted.
{"traces": [{"traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a1b2c3d4e5f60003", "parentSpanId": "a1b2c3d4e5f60002","service": "checkout", "name": "POST /checkout", "kind": "SERVER","startTime": "2026-10-01T12:00:00.007Z", "durationMs": 395,"attributes": {"http.request.method": "POST", "http.route": "/checkout"},"resource": {"service.version": "1.5.0", "telemetry.sdk.language": "java", "telemetry.sdk.version": "1.43.0"},"scope": {"name": "io.opentelemetry.spring-webmvc-6.0"}}]}
OTLP/JSON. Wrap the document in a list. IDs can be hex (as the OTLP/JSON spec requires) or base64 (as protobuf MessageToJson produces).
{"traces": [{"resourceSpans": [{"resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "cart"}}]},"scopeSpans": [{"scope": {"name": "OpenTelemetry.Instrumentation.AspNetCore"}, "spans": [{"traceId": "5b8efff798038103d269b633813fc60c", "spanId": "eee19b7ec3c1b174", "name": "GET /cart/{id}", "kind": 2,"startTimeUnixNano": "1790000000000000000", "endTimeUnixNano": "1790000000032000000"}]}]}]}]}
Jaeger and Zipkin. Paste the API output as-is. For Jaeger, span.kind, otel.library.name / otel.scope.name and error / otel.status_code tags are understood, and FOLLOWS_FROM references become links. For Zipkin, remoteEndpoint.serviceName becomes peer.service and shared server spans are separated from their client half.
Logs (simplified).
{"logs": [{"service": "checkout", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a1b2c3d4e5f60003", "time": 1790000000000}]}
Output
Each trace set produces one dataset item in the shared Report envelope (product, target, score, risk_level, summary, findings[], data{}, errors[], disclaimer), so REPORT.md, SARIF and the CI gate work like in every other Actor in this family. The product payload is in data:
| Field | Content |
|---|---|
fleet_coverage_score | The fleet score (same value as score). |
stats | Traces, spans, services, resources, detected formats, time range, root and orphan span counts, collapsed mesh-proxy spans. |
services[] | Per service: score, breakdown{propagation,depth,metadata,correlation}, spans, traces, span kinds, instances, versions, languages, SDK versions, auto_instrumented, entry / root-entry / orphan / outbound span counts, calls, called_by, finding counts. Log correlation share is included when logs were supplied. Sorted worst first. |
dependency_graph | nodes[] (type service / database / cache / messaging / saas / external / internal-unknown, plus a suspected_dark flag), edges[] (calls, paired calls, status traced / partially traced / client-only / suspected uninstrumented) and mermaid text. |
dark_dependencies[] | Dependency, how it was identified, callers with call counts, hosts, operations, example trace ID. |
propagation_breaks[] | Matched client → server pairs across traces: both trace IDs, start offset, and what the match was based on. |
gaps[] | Per service and operation: low-coverage / opaque, occurrences, median child coverage, median uninstrumented ms, largest contiguous gap. |
route_coverage[] | Routes with inconsistent downstream coverage, by downstream type and broken down by version. |
scope_inventory | Per service: instrumentation scopes, versions, span counts, whether each is auto-instrumentation. |
correlation | Log and metric join statistics, or null when no logs or metrics were supplied. |
improvement_plan[] | Ranked actions (see below). With a baseline, each action lists regressed_services, and actions touching them get 1.5× priority. |
regressions | Only with a baseline (otherwise null): baseline and current fleet score and delta, per-service baseline_score / score / delta / status (regressed at −5 points or worse, improved, stable), regressed_services, improved_services, new_services, missing_services, and new_findings / resolved_findings (when the baseline carried findings). |
data_quality[] | Notes about the input: mixed formats, base64 IDs, missing timestamps, duplicates, small sample, truncation. |
thresholds, billing, notes | Effective thresholds (min_gap_ms, gap_ratio, clock_skew_ms), billed units, run notes. |
Example finding:
{"rule_id": "COV-PROP-003", "severity": "high", "kind": "observation", "category": "propagation","title": "Suspected propagation break checkout → inventory (6 matched call(s))","location": "edge:checkout->inventory", "confidence": 0.9,"evidence": {"services": ["checkout", "inventory"], "matched_calls": 6, "matched_on_route": 6,"examples": [{"client_trace_id": "1234…", "server_trace_id": "9f0e…", "start_offset_ms": 3.0,"matched_on": "timing + route/method"}]}}
Example Mermaid graph from the demo fleet. Dashed edges are untraced or partially traced, and dashed red nodes are suspected dark dependencies.
graph LRn3["checkout"]n5["fraud-check (no spans?)"]:::darkn6["frontend"]n7["inventory"]n10[("postgresql:orders-db.shop.svc.cluster.local")]n6 -->|"18"| n3n3 -.->|"18 suspected uninstrumented"| n5n3 -.->|"18 partially traced"| n7n3 -->|"12"| n10classDef dark stroke-dasharray: 5 5,stroke:#d9534f
Improvement plan example
[{"rank": 1, "action": "set-service-identity", "title": "Name every service and describe its deployment","effort": "low", "impact": "high", "services": ["recommendation", "unknown_service:java"],"rules": ["COV-META-001", "COV-META-002"],"hints": {"generic": "Set OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES=service.version=<version>,deployment.environment.name=<env> …","java": "-Dotel.service.name=checkout -Dotel.resource.attributes=service.version=${GIT_SHA} …","python": "Set the env vars before `opentelemetry-instrument`, or Resource.create({...}) in code."}},{"rank": 2, "action": "fix-propagation-boundaries", "title": "Repair broken propagation boundaries","effort": "medium", "impact": "high", "services": ["checkout", "email-worker", "inventory"]},{"rank": 3, "action": "data-instrumentation", "title": "Instrument database, cache and messaging clients","effort": "low", "impact": "high", "services": ["inventory", "recommendation"],"hints": {"python": "pip install opentelemetry-instrumentation-psycopg (or -psycopg2, -sqlalchemy, -asyncpg, -redis, -pymongo)."}}]
Per-language hints cover java, python, nodejs, go and dotnet. Only the languages detected in the affected services (telemetry.sdk.language) are included. If no language is known, all hints are included.
Pricing
This Actor uses pay-per-event pricing. You pay only for results, and Apify platform usage is included.
| Event | Price | When it is charged |
|---|---|---|
coverage-audited | $0.75 | Once per audited trace set of up to 10,000 spans. Larger sets are charged one event per started 10,000 spans (ceil(spans / 10,000)), up to 100,000 spans per set. |
correlation-check | $0.25 | Once per trace set, only when you supplied logs and/or metrics and the correlation actually ran. |
- Trace sets that can't be parsed (no spans found) are free, and per-service items are free.
- Before each set, the Actor checks that your remaining budget ("maximum cost per run") covers the combined cost of all of that set's units (plus the correlation check, when requested). If it doesn't, the run stops before doing unpaid work. If the budget covers the audit but not the correlation check, the audit runs without correlation and you aren't charged for it.
- Examples: one 4,000-span trace set = $0.75. The same set with logs = $1.00. Three environments of 25,000 spans each, with metrics = 3 × (3 × $0.75 + $0.25) = $7.50.
Plans and licensing
Apify Store users get the full product on pay-per-event pricing, including SARIF export and the CI gate. License keys (licenseKey) unlock more: Pro lets you disable rules (disabledRules) and audit more trace sets per run, Team adds bulk runs, and Enterprise adds white-label reports. Prepaid keys skip pay-per-event charges. Local runs without a key use the Community plan, which allows up to 3 trace sets per run. Missing features never fail a run; they are skipped with a note. See ../../docs/licensing.md.
Limitations
- Inference, not proof. Every gap is a suspected coverage gap. Head sampling, SDK queue overflows, collector drops and partial exports look the same as missing instrumentation. The
confidencevalue and thedata_qualitynotes help you judge each one. - Sample size matters. Coverage is a fleet property. Fewer than about 10 traces gives weak conclusions (
COV-DATA-001). A few hundred traces covering your main routes works well. - Timing-based matching depends on clocks. Propagation-break matching allows 100 ms of clock skew by default (
clockSkewMs) and requires compatible route and method when both sides record them. Very busy routes without route attributes can produce ambiguous matches, which get lower confidence. - Host classification is heuristic. "Internal" hosts are recognised by naming (no dot,
*.svc.cluster.local,*.internal, private IPs, …). The SaaS list is curated. Setpeer.serviceordb.systemon client spans, or provideexpectedServices, to remove ambiguity. - The SDK version table is approximate (a snapshot from this release). It only flags clearly old SDKs.
- Zipkin and Jaeger exports carry less metadata (no scopes, partial resources), so metadata and scope checks are less precise for them.
- Input size. Inline input is limited to a few MB; use a dataset or key-value store record for larger exports. Each set is capped at 100,000 spans (and 200,000 dataset items); whole traces beyond that are skipped, with a note. Logs and metrics must still be inline.
- Heuristics are tuned on synthetic fleets. Regression fixtures cover async messaging (child and linked consumers), cache-hit routes, batch jobs and Istio sidecars, but real exports can still produce false positives. Use
disabledRulesand report patterns that look wrong. - No network access. The Actor never fetches traces from your backend. You send it exported data (inline or in Apify storage).
FAQ
Does any of my telemetry leave Apify? No. The Actor makes no network calls outside Apify (it only reads the Apify datasets or records you point it to). Log bodies and attribute values are never copied to the output. Evidence contains service names, span names, hosts, routes, IDs and counts.
A database shows up as a dependency without spans. Is that a problem?
No. Databases, caches, brokers (db.system, messaging.system) and SaaS or external hosts are classified as managed or external and are never reported as dark dependencies. Only internal hosts and peer.service names that never emit spans are reported.
Why is a dark dependency reported when the service is instrumented?
Usually because its service.name doesn't match the name or host the callers use, or because it exports to a different backend. Add it to expectedServices, or set peer.service on the client spans to the callee's service.name.
Why does COV-GAP-001 fire on an endpoint that is pure CPU work?
Uncovered time can be CPU work. The finding means "this time isn't attributed". Add manual INTERNAL spans if the time matters, or raise minGapMs / lower gapRatio.
How do I run it in CI?
Export traces from an integration-test run (Collector file exporter), call the Actor with failOnSeverity: "high", and upload REPORT.sarif to GitHub code scanning. To catch coverage regressions, pass the previous run's dataset as baselineDatasetId and read data.regressions.
How do I silence a rule?
Use disabledRules (plans with custom rules). Rule IDs are stable.
Changelog
See CHANGELOG.md.