# OTel PII & Secrets Leak Detector (`michiganpublictech/otel-pii-leak-detector`) Actor

Scan OpenTelemetry traces, metrics and logs for leaked secrets and PII before they reach your observability backend. Get a risk report, a Collector redaction config and sanitized telemetry. Standby HTTP API included.

- **URL**: https://apify.com/michiganpublictech/otel-pii-leak-detector.md
- **Developed by:** [Justin Hagerty](https://apify.com/michiganpublictech) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $250.00 / 1,000 telemetry batch scanneds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## OTel PII & Secrets Leak Detector

**Scan OpenTelemetry traces, logs and metrics for API keys, tokens, passwords, emails, card numbers and other personal data before they reach your observability backend. You get a risk report, a ready-to-review OpenTelemetry Collector redaction config, and a sanitized copy of your telemetry.**

Telemetry leaks quietly. An HTTP instrumentation captures the `Authorization` header. An exception message echoes a database URL with its password. A developer adds `user.email` to a span "just for debugging", or a log line prints a whole request body containing a card number. A few weeks later that data has been copied to every observability backend, retained for 30 to 400 days and made searchable by hundreds of engineers and third-party vendors.

This Actor reads OTLP/JSON (the format the Collector `file` exporter, `otlphttp` with JSON encoding and SDK debug exporters write) and runs **66 rules** against every value: resource and scope attributes, span names and attributes, span events (`exception.message`, `exception.stacktrace`), links, status messages, log bodies (including nested structured bodies), log attributes, metric names and descriptions, data-point attributes and exemplars. For each leak it tells you **which service, scope and attribute key** leaks, how often, with what confidence and with which potential regulatory exposure. It then generates the Collector processors that remove the leak, and checks its own output by re-scanning.

The analysis is deterministic. It uses no LLMs and makes no network calls, and **raw values are never stored**: findings carry redacted previews and salted fingerprints only.

***

### Why use it

- **Find leaks before your vendor does.** Secrets in telemetry are a common incident source, and a quiet one: logs and traces are rarely access-controlled like databases.
- **Reduce compliance exposure.** Personal data in telemetry brings your observability stack into scope for GDPR, CCPA, HIPAA or PCI DSS. The report labels *potential* exposure per regime (this is not legal advice).
- **Fix it, not just find it.** You get an `attributes` + `transform` (OTTL) + `redaction` processor config wired into a full pipeline, plus SDK-side hints to stop the leak at the source.
- **Prove the fix.** The sanitized copy of your telemetry is re-scanned (self-check), and the generated Collector config is *emulated* against your data, so the report lists exactly which leaks the config would still miss (for example, keys nested inside structured log bodies).
- **Low noise.** Checksums (Luhn, IBAN mod-97, ABA, Spanish DNI, Dutch BSN, Brazilian CPF, Aadhaar Verhoeff, PESEL), SSA invalid ranges, numbering-plan rules and context requirements filter out look-alikes such as order IDs, timestamps, trace IDs, UUIDs, pagination tokens and part numbers. On a ~200,000-value synthetic corpus of realistic benign telemetry, **no high-confidence finding was a false positive** (see [Limitations](#limitations)). Well-known test values (`4111 1111 1111 1111`, `AKIAIOSFODNN7EXAMPLE`, `example.com`, `555-01xx`, documentation IP ranges) are labelled as test values.

### Who it's for

- **Platform / observability teams** rolling out OpenTelemetry who need a pre-production gate for new instrumentation.
- **Security and privacy engineers** auditing what actually ends up in Datadog, Grafana, Honeycomb, New Relic, Elastic, Splunk and similar backends.
- **Teams in regulated industries** (health, fintech, e-commerce) who must keep PHI and cardholder data out of logs.
- **Pipelines and AI agents** that call it over HTTP (Standby mode) to check a telemetry sample before shipping a change.

### What it detects

| Group (`detectors`) | Rules | Examples |
|---|---|---|
| `secrets` | LEAK-001 – LEAK-021, LEAK-062 – LEAK-066 | AWS access/secret keys, GCP service-account keys, Google API keys, Azure account keys and SAS signatures, GitHub/GitLab/Slack/Stripe/Twilio/SendGrid/OpenAI/Anthropic tokens, npm/PyPI tokens, New Relic/Grafana/Datadog keys, Shopify tokens, HashiCorp Vault (`hvs.`/`hvb.`/`hvr.`) and Terraform Cloud tokens, Hugging Face tokens, Databricks PATs, Mailgun keys (with a Mailgun label), PEM private keys, JWTs (decoded structurally to report the algorithm and **which PII claims** they carry — never verified), high-entropy values in secret-named attributes, `password=`/`api_key:` assignments in free text |
| `credentials` | LEAK-022 – LEAK-031 | Captured `Authorization`, `Cookie`/`Set-Cookie`, `x-api-key`-style headers; Bearer/Basic credentials in messages (Basic is decoded to confirm `user:password`); session IDs; passwords in URLs (`scheme://user:pass@host`) and connection strings; tokens in URL query strings (`url.full`, `http.url`, `http.target`); `IDENTIFIED BY '…'` / `PASSWORD '…'` / Redis `AUTH` literals in `db.statement` / `db.query.text`; password-named attributes |
| `pii-contact` | LEAK-032 – LEAK-035 | Emails (also URL-encoded `%40`), phone numbers (E.164, international, North-American with numbering-plan checks), street addresses (heuristic), names in name-labelled attributes |
| `pii-identifiers` | LEAK-036 – LEAK-042, LEAK-059 – LEAK-061 | US SSNs (SSA rules), checksum-verified national IDs (ES DNI/NIE, NL BSN, UK NINO when labelled or printed `AB 12 34 56 C`, Brazilian CPF, Indian Aadhaar, Polish PESEL), passport / driver's licence / tax IDs, dates of birth, `user.*` / `enduser.*` identity attributes, IMEI (Luhn), advertising IDs |
| `network` | LEAK-043 – LEAK-045 | End-user IPs in `client.address`, `source.address`, `X-Forwarded-For` and similar (public vs. private told apart; personal data under GDPR), public IPs elsewhere, MAC addresses |
| `financial` | LEAK-046 – LEAK-049 | Card numbers (Luhn + issuer prefix/length + printed grouping), CVV/expiry attributes, IBANs (mod-97), bank account / ABA routing numbers |
| `health` | LEAK-050 – LEAK-052 | ICD-10 codes in diagnosis context, medical record / patient / health-plan IDs, health terms near patient context (PHI heuristic) |
| `location` | LEAK-053 | Precise coordinates (≥ 3 decimals) |
| `free-text` *(off by default)* | LEAK-054 – LEAK-055 | Names in prose (honorifics, `customer: First Last`), request/response/message bodies captured as attributes |
| — | LEAK-056 | Your own `customPatterns` |
| — | LEAK-057 – LEAK-058 | Placement: sensitive values in span/metric **names** (can't be deleted, explode cardinality) or in **resource attributes** (copied onto every signal) |

Every rule, with severity, rationale and references, is listed in [RULES.md](RULES.md).

### How to use

#### In Apify Console

1. Open the Actor and paste an OTLP/JSON export into **Telemetry** (the prefilled demo uses obviously fake values). You can also paste JSON Lines from the Collector `file` exporter into **Telemetry as text**.
2. Optionally pick detector groups, regimes, allowlists and custom patterns.
3. Run. The **Findings** view lists each leak by `signal/service/attribute`. The **Risk report** view shows category counts, top leaking keys and potential regulatory exposure. **Collector config & sanitized telemetry** has the YAML and the cleaned copy.

#### Capture a sample from your Collector

```yaml
exporters:
  file/sample:
    path: /tmp/otel-sample.jsonl     # OTLP/JSON, one export request per line
service:
  pipelines:
    traces:  { receivers: [otlp], processors: [batch], exporters: [file/sample, otlp/backend] }
    logs:    { receivers: [otlp], processors: [batch], exporters: [file/sample, otlp/backend] }
```

Prefer a **staging environment with synthetic traffic** (see [Data handling](#data-handling)).

#### Via the API (curl)

```bash
curl -s -X POST "https://api.apify.com/v2/acts/michiganpublictech~otel-pii-leak-detector/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "telemetry": {
    "resourceLogs": [{
      "resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "checkout"}}]},
      "scopeLogs": [{"logRecords": [
        {"body": {"stringValue": "payment failed for jane.doe@example.com card 4111 1111 1111 1111"},
         "attributes": [{"key": "http.request.header.authorization", "value": {"stringValue": "Basic ZGVtbzpkZW1v"}}]}
      ]}]
    }]
  },
  "redactionStrategy": "balanced"
}
EOF
```

#### Python (apify-client)

```python
import json
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
batches = [json.loads(line) for line in open("/tmp/otel-sample.jsonl")]
run = client.actor("michiganpublictech/otel-pii-leak-detector").call(
    run_input={"telemetryBatches": batches[:20], "failOnSeverity": "high"}
)
for report in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(report["target"], report["risk_level"], report["summary"])
    for f in report["findings"]:
        print(" ", f["severity"], f["rule_id"], f["location"])
```

#### Standby mode (HTTP API for pipelines and AI agents)

The Actor supports [Actor Standby](https://docs.apify.com/platform/actors/running/standby). Apify keeps a warm instance behind an HTTPS endpoint, so calls skip container start-up. Charging is identical to normal runs.

```bash
BASE=https://michiganpublictech--otel-pii-leak-detector.apify.actor

## Free: help, rule catalog, detector groups
curl -s "$BASE/" -H "Authorization: Bearer $APIFY_TOKEN"
curl -s "$BASE/rules" -H "Authorization: Bearer $APIFY_TOKEN"

## Scan: POST a raw OTLP/JSON document...
curl -s -X POST "$BASE/scan" -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  --data @otlp-export.json

## ...or an input-shaped object with options
curl -s -X POST "$BASE/scan" -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"telemetry": {"spans": [{"name": "GET /x", "attributes": {"url.full": "https://a.example.com/?token=abc123def456"}}]},
       "detectors": ["secrets", "credentials"], "emitSanitizedTelemetry": false}'
## → {"items": [<report>], "charged": {"telemetry-scanned": 1, "sanitized-output": 1}}
```

Endpoints: `GET /` (health/help; also answers the readiness probe), `GET /rules`, `GET /detectors`, `POST /scan`. Request bodies are limited to 8 MB. Options you don't send fall back to the Standby run's input. In Standby, results are returned in the HTTP response only and nothing is written to a dataset. The server never logs request bodies.

#### CI: block leaky instrumentation in pull requests

Run your integration tests with the Collector `file` exporter, then:

```yaml
- name: Scan telemetry sample for PII & secrets
  run: |
    jq -Rs '{telemetryText: ., failOnSeverity: "high", sampleRetention: "none"}' otel-sample.jsonl > input.json
    curl -sf -X POST "https://api.apify.com/v2/acts/michiganpublictech~otel-pii-leak-detector/runs?token=${{ secrets.APIFY_TOKEN }}&waitForFinish=300" \
      -H "Content-Type: application/json" -d @input.json | jq -e '.data.status == "SUCCEEDED"'
```

`failOnSeverity` marks the run as FAILED when any finding reaches that severity. `REPORT.sarif` can be uploaded to GitHub code scanning.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `telemetry` | object (or list) | demo | One OTLP/JSON export request (`resourceSpans` / `resourceLogs` / `resourceMetrics`; snake_case and legacy `instrumentationLibrary*` keys accepted), or a simplified `{resource, scope, spans, logs, metrics}` object with plain attribute maps. A list counts as several batches. |
| `telemetryBatches` | array | — | Several batches; each an OTLP/JSON document or `{"name": "...", "telemetry": {...}}`. One report per batch. |
| `telemetryText` | string | — | OTLP/JSON text, a JSON array, or JSON Lines (one batch per line). Invalid lines become free failed reports. |
| `detectors` | array | all except `free-text` | Detector groups to run (see the table above). |
| `regimes` | array | all | `GDPR`, `CCPA`, `HIPAA`, `PCI_DSS` — only used to **label** potential exposure. |
| `minConfidence` | `low`/`medium`/`high` | `low` | Drop detections below this confidence. |
| `testValues` | `downgrade`/`report`/`ignore` | `downgrade` | Known test values are reported with LOW severity by default. |
| `allowlistAttributes` | array | — | Keys never reported; exact or glob (`app.public_*`), matched against the full key and its last segment. |
| `allowlistPatterns` | array | — | Regexes; a detection whose text (or whole value) fully matches is ignored. ReDoS-screened. |
| `customPatterns` | array | — | Up to 20 `{name, pattern, severity, action}` detectors. ≤ 300 chars; nested quantifiers `(a+)+`, overlapping repeated alternations `(a|ab)+` and backreferences are rejected. RE2-compatible patterns also go into the Collector config. |
| `emitSanitizedTelemetry` | boolean | `true` | Return a sanitized copy plus a re-scan self-check. |
| `generateCollectorConfig` | boolean | `true` | Generate the Collector redaction config, SDK hints and the coverage emulation. |
| `redactionStrategy` | `balanced`/`mask`/`hash`/`drop` | `balanced` | `balanced`: delete credential headers, hash identifiers (`user.email`, `client.address`…), mask the rest. `hash`: hash whole values where possible (keeps joinability). `drop`: delete leaking attributes and emit a redaction `allowed_keys` allowlist. |
| `collectorFlavor` | `contrib`/`contrib-legacy` | `contrib` | Target otelcol-contrib version family: `contrib` for current releases (verified 0.148, 0.150), `contrib-legacy` for 0.115-era Collectors (see [Collector version notes](#collector-version-notes)). |
| `hashSalt` | secret string | random per run | Salts fingerprints and `[HASH:…]` tokens so they're comparable across runs. |
| `sampleRetention` | `redacted`/`none` | `redacted` | `none` removes previews and fingerprints from findings (counts and locations only). |
| `minSeverity` | severity | `info` | Hide findings below this severity (risk counts still include them). |
| `failOnSeverity` | `none`/`critical`/`high`/`medium`/`low` | `none` | CI gate. |
| `disabledRules` | array | — | Rule IDs to skip (Pro plan and above). |
| `licenseKey` | secret string | — | Enterprise / prepaid license key. Store users don't need one. |

### Output

One dataset item per batch, using the same report shape as all our Actors. The key-value store also gets `OUTPUT` (run summary), `REPORT.md` and `REPORT.sarif`. Abridged example from the prefilled demo:

```json
{
  "product": "otel-pii-leak-detector",
  "target": "telemetry",
  "score": 26,
  "risk_level": "critical",
  "summary": {"critical": 2, "high": 6, "medium": 6, "low": 10, "info": 0},
  "findings": [
    {
      "rule_id": "LEAK-022",
      "title": "Authorization header captured as an attribute",
      "severity": "critical",
      "kind": "observation",
      "category": "credential",
      "location": "traces/checkout/span.attributes[\"http.request.header.authorization\"]",
      "confidence": 0.95,
      "evidence": {
        "occurrences": 1, "distinct_values": 1, "confidence": "high", "detector": "authorization-header",
        "scopes": ["io.opentelemetry.http"],
        "example_path": "$.resourceSpans[0].scopeSpans[0].spans[0].attributes[2].value.arrayValue.values[0].stringValue",
        "potential_regulatory_exposure": ["CCPA"],
        "redacted_previews": ["Bear…Ww (187 chars)"], "value_fingerprints": ["5d1c0a9e44b2"],
        "details": {"scheme": ["bearer"]}
      },
      "recommendation": "Remove Authorization from captured request headers and delete the attribute in the Collector; …"
    },
    {
      "rule_id": "LEAK-017", "title": "JWT payload carries personal data claims", "severity": "high",
      "evidence": {"details": {"pii_claims": ["email", "name"]}, "redacted_previews": ["eyJ…"], "...": "..."}
    },
    {
      "rule_id": "LEAK-046", "title": "Payment card number (PAN) in telemetry (known test value)", "severity": "low",
      "location": "logs/payments/log.attributes[\"payment.card_number\"]",
      "evidence": {"redacted_previews": ["************1111"], "known_test_value": true, "...": "..."}
    }
  ],
  "data": {
    "signals": ["traces", "logs", "metrics"],
    "scanned": {"spans": 2, "span_events": 1, "log_records": 2, "data_points": 1, "records": 5, "values_scanned": 34,
                "bytes": 3459, "billable_units": 1},
    "risk": {
      "total_occurrences": 24,
      "by_category": {"credential": {"findings": 2, "occurrences": 2, "max_severity": "critical"}, "...": {}},
      "by_service": [{"service": "checkout", "occurrences": 15, "categories": ["credential", "secret", "..."],
                      "max_severity": "critical"}],
      "top_leaking_keys": [{"signal": "traces", "location": "span.attributes[\"http.request.header.authorization\"]",
                            "occurrences": 3, "rules": ["LEAK-016", "LEAK-017", "LEAK-022"], "max_severity": "critical"}],
      "regulatory_exposure": {"GDPR": {"potential_exposure": true, "occurrences": 18, "categories": ["..."]},
                              "PCI_DSS": {"potential_exposure": true, "...": "..."}},
      "legal_notice": "Regulatory labels … indicate POTENTIAL exposure … not legal advice …"
    },
    "collector_config": "# Generated by OTel PII & Secrets Leak Detector — REVIEW BEFORE DEPLOYING.\n…",
    "collector_config_coverage": {"residual_high_confidence": 3, "residual_locations": ["logs:payments:log.body.patient.dob", "..."]},
    "sdk_hints": ["Don't capture credential-bearing HTTP headers: …"],
    "sanitized_telemetry": {"resourceSpans": ["… same structure, values masked / hashed / deleted …"]},
    "sanitization": {"values_changed": 18, "masked_spans": 16, "hashed_values": 5, "deleted_attributes": 1},
    "self_check": {"residual_high_confidence": 0, "shared_secret_scanner_hits": 0, "passed": true}
  },
  "disclaimer": "Automated pattern-based scan of the telemetry you supplied; …"
}
```

#### Generated Collector config (excerpt)

```yaml
processors:
  memory_limiter: {check_interval: 1s, limit_percentage: 80, spike_limit_percentage: 25}
  attributes/leak_keys:
    actions:
    - key: http.request.header.authorization
      action: delete
  transform/leak:
    error_mode: ignore
    trace_statements:
    - context: span
      statements:
      - set(attributes["user.id"], SHA256(Concat([attributes["user.id"], "${env:PII_HASH_SALT}"], ""))) where attributes["user.id"] != nil
      - replace_all_patterns(attributes, "value", "eyJ[A-Za-z0-9_-]{8,}[.]eyJ[A-Za-z0-9_-]{8,}[.][A-Za-z0-9_-]*", "[REDACTED:jwt]")
      - replace_all_patterns(attributes, "value", "([?&;](access_token|...|api_key|...)=)[^&# ]+", "$${1}[REDACTED]")
      - replace_pattern(name, "[A-Za-z0-9._%+-]+(@|%40)...", "[REDACTED:email]")
    log_statements:
    - context: log
      statements:
      - replace_pattern(body, "(AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}", "[REDACTED:aws_key]") where IsString(body)
  redaction/leak:
    allow_all_keys: true
    ignored_keys: [user.id]          # hashed keys: keep the hash instead of masking it to ****
    blocked_values: ["(AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}", "…"]
    blocked_key_patterns: ["(?i).*(authorization|password|passwd|pwd|passphrase).*"]
    summary: info
  batch: {send_batch_size: 8192, timeout: 5s}
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, attributes/leak_keys, transform/leak, redaction/leak, batch]
      exporters: [otlp_grpc/backend]   # contrib-legacy: otlp/backend
```

How it is built:

- **`attributes/leak_keys`** deletes credential-bearing keys observed on spans, logs and data points.
- **`transform/leak`** (OTTL) hashes identifiers with a salted `SHA256(Concat([…, "${env:PII_HASH_SALT}"], ""))` instead of deleting them, so you can still count distinct users. It deletes or masks keys where the attributes processor can't reach them (resource, scope, span events) and rewrites values with `replace_all_patterns` / `replace_pattern` (attributes, span names, status messages, string log bodies, metric descriptions).
- **`redaction/leak`** is a safety net. Its `blocked_values` are the patterns above plus a baseline of high-impact secret patterns. It also gets `blocked_key_patterns` (`contrib`) and `ignored_keys` for the keys the transform hashed — without that, a blocked key pattern would overwrite the hash with `****`.
- With `redactionStrategy: drop` the redaction processor runs with `allow_all_keys: false` and an `allowed_keys` list of **every key observed in your sample that didn't leak** (resource and scope attributes, and log-body map keys for `contrib`, included; `service.name` and the other `service.*` identity keys always). Any key that is not on the list is removed, including resource attributes, so add the keys your services emit that the sample didn't contain.
- All patterns are written in the RE2 ∩ Python subset without quotes or backslashes, so they survive YAML and OTTL quoting unchanged.
- `collector_config_coverage` is a Python emulation of the config on your data, followed by a re-scan. It lists what the config would **not** catch (typically values inside array or map attributes, which neither OTTL's `replace_all_patterns` nor the redaction processor rewrite, and — on `contrib-legacy` — nested log-body maps). Fix those with the SDK hints. The emulation was checked against real Collectors (see below) and predicted their output exactly, apart from the hash format (`[HASH:…]` vs SHA-256 hex) and hashed arrays (the Collector hashes the whole stringified array).

#### Collector version notes

Generated configs were checked with `otelcol-contrib validate` on **0.115.1, 0.148.0 and 0.150.1**, and run end to end (an `otlpjsonfile` receiver feeding the generated processors into a `file` exporter) on 0.115.1 and 0.150.1 with every redaction strategy. What we found:

| | 0.115.1 (`contrib-legacy`) | 0.148.0 / 0.150.1 (`contrib`) |
|---|---|---|
| `redaction` processor in traces, logs **and** metrics pipelines | yes | yes |
| `blocked_key_patterns` | **rejected** (`'' has invalid keys`) | yes |
| `ignored_keys`, `allowed_keys`, `allow_all_keys`, `summary` | yes | yes |
| Redaction masks resource, span, log-record and data-point attributes | yes | yes |
| … also scope and span-event attributes, and log bodies (strings **and** nested maps/arrays) | no | yes |
| `allow_all_keys: false` removes non-listed keys from resource attributes | yes | yes (also scope, span-event, log-body keys) |
| Exporter type | `otlp` (no `otlp_grpc`) | `otlp_grpc` (`otlp` logs a deprecation warning on 0.150.1) |

Behaviour common to all three versions:

- The OTTL used here parses in every version: `set`, `delete_key`, `replace_pattern`, `replace_all_patterns(attributes, "value", …)`, `SHA256(Concat([…], ""))`, `IsString(body)` and the `context:` + `statements:` block form (no deprecation warning on 0.150.1).
- `$$` escapes `$` in Collector config files, so `$${1}` reaches OTTL as a `${1}` capture-group reference. That worked as intended on all versions. Hashing needs the `PII_HASH_SALT` env var (alphanumeric); `SHA256` produces 64 hex characters.
- Neither the transform nor the redaction processor rewrites values inside **array or map attributes**, and the redaction processor never touches span names, status messages, links or metric descriptions (the transform covers those that leaked).
- The redaction processor runs after the transform, so it can re-mask what the transform already replaced: on 0.150.1, `password=[REDACTED]` in a log body becomes `****` because a blocked value matches it. The data stays hidden either way.

Pick `contrib-legacy` for 0.115-era Collectors and `contrib` for current releases. Versions between 0.116 and 0.147 were not tested: if `validate` rejects `blocked_key_patterns` or `otlp_grpc`, use `contrib-legacy`. **Always review and run `otelcol-contrib validate --config=<file>` before deploying.** Generated configs are a starting point, not a guarantee.

### Regulatory mapping

| Regime | Labelled when we detect | Basis |
|---|---|---|
| GDPR | contact details, identifiers, IPs/MACs/device IDs, location, financial, health (Art. 9 special category), JWTs with PII claims | personal data (Art. 4) |
| CCPA / CPRA | the above plus account credentials (password, auth headers, session tokens) | personal and sensitive personal information |
| HIPAA | health detectors (ICD-10, MRN, PHI heuristics); identifiers are labelled HIPAA only when health data appears in the same batch | PHI = health information + identifiers |
| PCI DSS | card numbers (PAN) and card verification data | cardholder data / sensitive authentication data |

> **Not legal advice.** Labels mean *potential exposure* based on the data types detected in your sample. They are not a compliance assessment or certification. Involve your privacy, security and compliance teams.

### Data handling

- **Prefer synthetic or sampled telemetry.** Use staging traffic, test accounts and test cards where possible. The scanner tells you whether a field *captures* a kind of data even when the value is a known test value.
- **What we store:** findings with redacted previews (for example `AKIA…LE (20 chars)`, `j***@***.com`, `************1111`; passwords always `****`) and salted HMAC fingerprints (12 hex chars). Set `sampleRetention: none` to drop previews and fingerprints entirely. The sanitized telemetry contains masked/hashed values only, and the self-check verifies that. The generated config contains patterns, never your values.
- **What we never store or log:** raw secret or PII values. Your input itself is stored by Apify as the run's `INPUT` record, like any Actor input.
- **Retention:** Apify's default storage retention applies to the run's input, dataset and key-value store (unnamed storages expire automatically; see the Apify docs on data retention). Delete the run when you are done if your sample was sensitive.
- `hashSalt` and `licenseKey` are secret inputs (encrypted by Apify) and never echoed into output.

### Pricing

Pay per event, with no subscription.

| Event | Price | When |
|---|---|---|
| `telemetry-scanned` | **$0.25** | Per scanned batch. One event covers up to **5 MB** of OTLP/JSON **and** up to **10,000 records** (spans + log records + metric data points). Larger batches count `max(ceil(MB / 5), ceil(records / 10,000))` events, up to 20 (bigger batches are refused for free; split them). |
| `sanitized-output` | **$0.10** | Per batch, only when a remediation pack (sanitized telemetry and/or Collector config) with **at least one change** was delivered. |

Examples: one 300-span trace export with leaks costs $0.25 + $0.10 = **$0.35**. A clean batch costs **$0.25**. A 25,000-span batch with remediation costs 3 × $0.25 + $0.10 = **$0.85**. Batches that can't be parsed, and batches we refuse, are **never charged**. The Actor checks your spending limit before each batch and drops the remediation pack, rather than charging partially, if only the scan fits.

### Plans and licensing

Store users need nothing extra. Sanitized telemetry and the generated config are included on the Apify Store (marketplace) plan. Self-hosted/community runs produce the report only. `disabledRules` needs a Pro license. Enterprise and prepaid keys go in `licenseKey`.

### Limitations

- **Heuristic detection.** Names, addresses and health context use heuristics (low/medium confidence), and novel token formats without a recognisable shape are only caught when the attribute name is secret-like. False positives and false negatives are possible. Check `confidence` on each finding.
- **Measured precision (synthetic data).** On a generated corpus of ~197,000 benign values (trace IDs, UUIDs, digests, k8s names, versions, timestamps, base64 cursors, URLs, order and invoice numbers, part numbers, stack traces…) plus 603 planted leaks, every planted leak was found, no high-confidence finding was a false positive, and about 1 in 7,500 benign values raised a medium-confidence finding (mostly formatted CPF look-alikes). Real telemetry differs from synthetic data, so treat these figures as a regression baseline rather than a guarantee. Use `minConfidence: high` for the quietest output.
- **Only what you send.** The report covers the sample, not everything your services emit. Rare code paths may leak in ways a sample doesn't show.
- **OTLP/JSON only.** Protobuf, Jaeger and Zipkin formats aren't parsed. Use the Collector `file` exporter (JSON) to capture. `bytesValue` attributes are not decoded.
- **Values longer than 64 KiB** are scanned up to that length, and at most 500,000 values per batch.
- **The Collector config can't reach everything.** Nested map values and metric names need source fixes. The coverage emulation tells you which locations are affected.
- **Collector patterns have no checksums or context.** OTTL/RE2 can't run Luhn, mod-97, mod-11 or Verhoeff, so the generated card, IBAN, SSN, CPF and national-ID patterns may also mask look-alike values (card patterns are restricted to issuer prefixes). The Mailgun pattern can mask `key-<32 hex>` cache keys. Review them against your data.

### FAQ

**Does it call any external service or LLM?** No. Detection, JWT decoding and config generation are local and deterministic.

**Why is `4111 1111 1111 1111` reported at all?** It's a test value, so severity is lowered to LOW. But a card-shaped value in `payment.card_number` means the attribute captures card numbers, and real ones would leak the same way. Set `testValues: ignore` to hide them.

**Will hashing `user.email` keep my dashboards working?** Yes for counting and grouping, because the same input gives the same salted hash. Hashing is pseudonymisation, not anonymisation.

**Why do I get both LEAK-022 and LEAK-016 for one header?** LEAK-022 says the header is captured at all. LEAK-016/017 describe the token inside it (and whether its payload carries personal data).

**Can I add my own identifiers?** Yes: `customPatterns` (for example `CUST-[0-9]{8}`) with severity and action. They're ReDoS-screened and added to the generated config when RE2-compatible.

**Is my data used for anything else?** No. See [Data handling](#data-handling).

### Roadmap

Planned detectors, outputs and known caveats are tracked in [STATUS.md](STATUS.md).

### Changelog

See [CHANGELOG.md](CHANGELOG.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/michiganpublictech/otel-pii-leak-detector/changelog.md

# Actor input Schema

## `telemetry` (type: `object`):

One OTLP/JSON export request: any of resourceSpans / resourceLogs / resourceMetrics (what the Collector file exporter, otlphttp JSON and SDK debug exporters produce). A simplified object {"resource": {...}, "spans": \[...], "logs": \[...], "metrics": \[...]} with plain attribute maps is accepted too. A list is treated as several batches.

## `telemetryBatches` (type: `array`):

Several batches, each an OTLP/JSON document or {"name": "...", "telemetry": {...}}. One report per batch.

## `telemetryText` (type: `string`):

OTLP/JSON text, a JSON array of batches, or JSON Lines (one batch per line — e.g. the Collector file exporter output). Lines that aren't JSON are reported as failed batches and never charged.

## `detectors` (type: `array`):

Which detector groups to run. Free-text heuristics are off by default (lower confidence).

## `regimes` (type: `array`):

Only used to LABEL potential exposure (GDPR, CCPA, HIPAA, PCI DSS). Not legal advice or a compliance certification.

## `minConfidence` (type: `string`):

Drop detections below this confidence (they are still counted in data.suppressed).

## `testValues` (type: `string`):

What to do with well-known test/documentation values (4111 1111 1111 1111, AKIAIOSFODNN7EXAMPLE, example.com, 555-01xx, documentation IP ranges). They still prove the attribute captures that kind of data.

## `allowlistAttributes` (type: `array`):

Attribute keys never reported (exact or glob, e.g. 'app.public\_\*' or 'user.id'). Matched against the full key and the last segment.

## `allowlistPatterns` (type: `array`):

Regular expressions; a detection whose matched text (or whole value) fully matches one is ignored. Patterns are screened for catastrophic backtracking.

## `customPatterns` (type: `array`):

Up to 20 extra detectors: \[{"name": "customer-ref", "pattern": "CUST-\[0-9]{8}", "severity": "medium", "action": "mask|hash|delete"}]. Max 300 characters; nested quantifiers such as (a+)+, overlapping repeated alternations and backreferences are rejected (ReDoS guard). RE2-compatible patterns are also added to the generated Collector config.

## `emitSanitizedTelemetry` (type: `boolean`):

Return a sanitized copy of each batch (masked / hashed / deleted values) plus a self-check that re-scans it. Charged as 'sanitized-output' only when at least one value changed.

## `generateCollectorConfig` (type: `boolean`):

Generate an OpenTelemetry Collector config (attributes, transform/OTTL and redaction processors wired between memory_limiter and batch) plus SDK-side hints, and report which leaks it would not catch.

## `redactionStrategy` (type: `string`):

How sanitized output and the generated config treat detected values.

## `collectorFlavor` (type: `string`):

Target distribution for the generated config. See the README for version caveats.

## `hashSalt` (type: `string`):

Optional secret (8–128 letters, digits, '\_' or '-') used to salt value fingerprints and \[HASH:…] tokens so they are comparable across runs. Default: a random per-run salt.

## `sampleRetention` (type: `string`):

Raw values are never stored. 'redacted' adds minimal previews such as 'AKIA…LE (20 chars)' or 'j\*\*\*@\*\*\*.com'.

## `minSeverity` (type: `string`):

Hide findings below this severity (risk summary counts still include them).

## `failOnSeverity` (type: `string`):

CI gate: mark the run as FAILED when any finding is at or above this severity.

## `disabledRules` (type: `array`):

Rule IDs to skip (see RULES.md), e.g. LEAK-044. Requires the custom_rules feature (Pro plan and above).

## `licenseKey` (type: `string`):

Optional. Enterprise / prepaid license key. Store users don't need one.

## Actor input object example

```json
{
  "telemetry": {
    "resourceSpans": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            },
            {
              "key": "deployment.environment.name",
              "value": {
                "stringValue": "demo"
              }
            }
          ]
        },
        "scopeSpans": [
          {
            "scope": {
              "name": "io.opentelemetry.http",
              "version": "2.9.0"
            },
            "spans": [
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "eee19b7ec3c1b174",
                "name": "GET /api/users/jane.doe@example.com/orders",
                "kind": 2,
                "attributes": [
                  {
                    "key": "http.request.method",
                    "value": {
                      "stringValue": "GET"
                    }
                  },
                  {
                    "key": "url.full",
                    "value": {
                      "stringValue": "https://shop.example.com/api/users/jane.doe@example.com/orders?api_key=demo-k3y-0f-fake-shop-9921&page=2"
                    }
                  },
                  {
                    "key": "http.request.header.authorization",
                    "value": {
                      "arrayValue": {
                        "values": [
                          {
                            "stringValue": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkphbmUgRG9lIiwiZW1haWwiOiJqYW5lLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMH0.ZGVtby1zaWduYXR1cmUtbm90LXJlYWw"
                          }
                        ]
                      }
                    }
                  },
                  {
                    "key": "client.address",
                    "value": {
                      "stringValue": "203.0.113.42"
                    }
                  },
                  {
                    "key": "user.id",
                    "value": {
                      "stringValue": "user-42"
                    }
                  },
                  {
                    "key": "app.customer_ref",
                    "value": {
                      "stringValue": "CUST-00012345"
                    }
                  },
                  {
                    "key": "geo.location.lat",
                    "value": {
                      "doubleValue": 42.3314
                    }
                  },
                  {
                    "key": "geo.location.lon",
                    "value": {
                      "doubleValue": -83.0458
                    }
                  }
                ],
                "events": [
                  {
                    "name": "exception",
                    "attributes": [
                      {
                        "key": "exception.type",
                        "value": {
                          "stringValue": "OperationalError"
                        }
                      },
                      {
                        "key": "exception.message",
                        "value": {
                          "stringValue": "could not connect to postgres://orders_app:Sup3rS3cretPw@db.internal:5432/orders"
                        }
                      }
                    ]
                  }
                ],
                "status": {
                  "code": 2,
                  "message": "upstream payment call failed"
                }
              },
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "aaa19b7ec3c1b175",
                "parentSpanId": "eee19b7ec3c1b174",
                "name": "SELECT orders",
                "kind": 3,
                "attributes": [
                  {
                    "key": "db.system",
                    "value": {
                      "stringValue": "postgresql"
                    }
                  },
                  {
                    "key": "db.query.text",
                    "value": {
                      "stringValue": "SELECT * FROM customers WHERE email = 'jane.doe@example.com' AND phone = '+1 202-555-0143'"
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    ],
    "resourceLogs": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "payments"
              }
            },
            {
              "key": "deployment.environment.name",
              "value": {
                "stringValue": "demo"
              }
            }
          ]
        },
        "scopeLogs": [
          {
            "scope": {
              "name": "payments.logger"
            },
            "logRecords": [
              {
                "severityText": "INFO",
                "body": {
                  "stringValue": "Loaded AWS credentials AKIAIOSFODNN7EXAMPLE from environment"
                },
                "attributes": [
                  {
                    "key": "payment.card_number",
                    "value": {
                      "stringValue": "4111 1111 1111 1111"
                    }
                  },
                  {
                    "key": "payment.iban",
                    "value": {
                      "stringValue": "GB82 WEST 1234 5698 7654 32"
                    }
                  }
                ]
              },
              {
                "severityText": "WARN",
                "body": {
                  "kvlistValue": {
                    "values": [
                      {
                        "key": "event",
                        "value": {
                          "stringValue": "patient_lookup"
                        }
                      },
                      {
                        "key": "patient",
                        "value": {
                          "kvlistValue": {
                            "values": [
                              {
                                "key": "mrn",
                                "value": {
                                  "stringValue": "MRN-00482913"
                                }
                              },
                              {
                                "key": "ssn",
                                "value": {
                                  "stringValue": "078-05-1120"
                                }
                              },
                              {
                                "key": "dob",
                                "value": {
                                  "stringValue": "1985-03-12"
                                }
                              }
                            ]
                          }
                        }
                      },
                      {
                        "key": "note",
                        "value": {
                          "stringValue": "Patient diagnosis E11.9, prescribed metformin"
                        }
                      }
                    ]
                  }
                },
                "attributes": []
              }
            ]
          }
        ]
      }
    ],
    "resourceMetrics": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            },
            {
              "key": "deployment.environment.name",
              "value": {
                "stringValue": "demo"
              }
            }
          ]
        },
        "scopeMetrics": [
          {
            "scope": {
              "name": "io.opentelemetry.http"
            },
            "metrics": [
              {
                "name": "http.server.request.duration",
                "unit": "s",
                "description": "Duration of HTTP server requests.",
                "histogram": {
                  "aggregationTemporality": 2,
                  "dataPoints": [
                    {
                      "count": "3",
                      "sum": 0.42,
                      "attributes": [
                        {
                          "key": "http.route",
                          "value": {
                            "stringValue": "/api/users/{id}/orders"
                          }
                        },
                        {
                          "key": "user.email",
                          "value": {
                            "stringValue": "john.smith@example.org"
                          }
                        }
                      ]
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "detectors": [
    "secrets",
    "credentials",
    "pii-contact",
    "pii-identifiers",
    "network",
    "financial",
    "health",
    "location"
  ],
  "regimes": [
    "GDPR",
    "CCPA",
    "HIPAA",
    "PCI_DSS"
  ],
  "minConfidence": "low",
  "testValues": "downgrade",
  "customPatterns": [
    {
      "name": "customer-ref",
      "pattern": "CUST-[0-9]{8}",
      "severity": "medium",
      "action": "hash"
    }
  ],
  "emitSanitizedTelemetry": true,
  "generateCollectorConfig": true,
  "redactionStrategy": "balanced",
  "collectorFlavor": "contrib",
  "sampleRetention": "redacted",
  "minSeverity": "info",
  "failOnSeverity": "none"
}
```

# Actor output Schema

## `results` (type: `string`):

One report per batch: score, risk level, findings, data.risk, data.collector_config and data.sanitized_telemetry.

## `summary` (type: `string`):

Plan, per-batch scores and billing summary for the run (OUTPUT record).

## `reportMarkdown` (type: `string`):

Human-readable report of all findings (REPORT.md).

## `reportSarif` (type: `string`):

SARIF 2.1.0 report for GitHub code scanning and other CI tools (REPORT.sarif).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "telemetry": {
        "resourceSpans": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "checkout"
                            }
                        },
                        {
                            "key": "deployment.environment.name",
                            "value": {
                                "stringValue": "demo"
                            }
                        }
                    ]
                },
                "scopeSpans": [
                    {
                        "scope": {
                            "name": "io.opentelemetry.http",
                            "version": "2.9.0"
                        },
                        "spans": [
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "eee19b7ec3c1b174",
                                "name": "GET /api/users/jane.doe@example.com/orders",
                                "kind": 2,
                                "attributes": [
                                    {
                                        "key": "http.request.method",
                                        "value": {
                                            "stringValue": "GET"
                                        }
                                    },
                                    {
                                        "key": "url.full",
                                        "value": {
                                            "stringValue": "https://shop.example.com/api/users/jane.doe@example.com/orders?api_key=demo-k3y-0f-fake-shop-9921&page=2"
                                        }
                                    },
                                    {
                                        "key": "http.request.header.authorization",
                                        "value": {
                                            "arrayValue": {
                                                "values": [
                                                    {
                                                        "stringValue": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkphbmUgRG9lIiwiZW1haWwiOiJqYW5lLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMH0.ZGVtby1zaWduYXR1cmUtbm90LXJlYWw"
                                                    }
                                                ]
                                            }
                                        }
                                    },
                                    {
                                        "key": "client.address",
                                        "value": {
                                            "stringValue": "203.0.113.42"
                                        }
                                    },
                                    {
                                        "key": "user.id",
                                        "value": {
                                            "stringValue": "user-42"
                                        }
                                    },
                                    {
                                        "key": "app.customer_ref",
                                        "value": {
                                            "stringValue": "CUST-00012345"
                                        }
                                    },
                                    {
                                        "key": "geo.location.lat",
                                        "value": {
                                            "doubleValue": 42.3314
                                        }
                                    },
                                    {
                                        "key": "geo.location.lon",
                                        "value": {
                                            "doubleValue": -83.0458
                                        }
                                    }
                                ],
                                "events": [
                                    {
                                        "name": "exception",
                                        "attributes": [
                                            {
                                                "key": "exception.type",
                                                "value": {
                                                    "stringValue": "OperationalError"
                                                }
                                            },
                                            {
                                                "key": "exception.message",
                                                "value": {
                                                    "stringValue": "could not connect to postgres://orders_app:Sup3rS3cretPw@db.internal:5432/orders"
                                                }
                                            }
                                        ]
                                    }
                                ],
                                "status": {
                                    "code": 2,
                                    "message": "upstream payment call failed"
                                }
                            },
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "aaa19b7ec3c1b175",
                                "parentSpanId": "eee19b7ec3c1b174",
                                "name": "SELECT orders",
                                "kind": 3,
                                "attributes": [
                                    {
                                        "key": "db.system",
                                        "value": {
                                            "stringValue": "postgresql"
                                        }
                                    },
                                    {
                                        "key": "db.query.text",
                                        "value": {
                                            "stringValue": "SELECT * FROM customers WHERE email = 'jane.doe@example.com' AND phone = '+1 202-555-0143'"
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ],
        "resourceLogs": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "payments"
                            }
                        },
                        {
                            "key": "deployment.environment.name",
                            "value": {
                                "stringValue": "demo"
                            }
                        }
                    ]
                },
                "scopeLogs": [
                    {
                        "scope": {
                            "name": "payments.logger"
                        },
                        "logRecords": [
                            {
                                "severityText": "INFO",
                                "body": {
                                    "stringValue": "Loaded AWS credentials AKIAIOSFODNN7EXAMPLE from environment"
                                },
                                "attributes": [
                                    {
                                        "key": "payment.card_number",
                                        "value": {
                                            "stringValue": "4111 1111 1111 1111"
                                        }
                                    },
                                    {
                                        "key": "payment.iban",
                                        "value": {
                                            "stringValue": "GB82 WEST 1234 5698 7654 32"
                                        }
                                    }
                                ]
                            },
                            {
                                "severityText": "WARN",
                                "body": {
                                    "kvlistValue": {
                                        "values": [
                                            {
                                                "key": "event",
                                                "value": {
                                                    "stringValue": "patient_lookup"
                                                }
                                            },
                                            {
                                                "key": "patient",
                                                "value": {
                                                    "kvlistValue": {
                                                        "values": [
                                                            {
                                                                "key": "mrn",
                                                                "value": {
                                                                    "stringValue": "MRN-00482913"
                                                                }
                                                            },
                                                            {
                                                                "key": "ssn",
                                                                "value": {
                                                                    "stringValue": "078-05-1120"
                                                                }
                                                            },
                                                            {
                                                                "key": "dob",
                                                                "value": {
                                                                    "stringValue": "1985-03-12"
                                                                }
                                                            }
                                                        ]
                                                    }
                                                }
                                            },
                                            {
                                                "key": "note",
                                                "value": {
                                                    "stringValue": "Patient diagnosis E11.9, prescribed metformin"
                                                }
                                            }
                                        ]
                                    }
                                },
                                "attributes": []
                            }
                        ]
                    }
                ]
            }
        ],
        "resourceMetrics": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "checkout"
                            }
                        },
                        {
                            "key": "deployment.environment.name",
                            "value": {
                                "stringValue": "demo"
                            }
                        }
                    ]
                },
                "scopeMetrics": [
                    {
                        "scope": {
                            "name": "io.opentelemetry.http"
                        },
                        "metrics": [
                            {
                                "name": "http.server.request.duration",
                                "unit": "s",
                                "description": "Duration of HTTP server requests.",
                                "histogram": {
                                    "aggregationTemporality": 2,
                                    "dataPoints": [
                                        {
                                            "count": "3",
                                            "sum": 0.42,
                                            "attributes": [
                                                {
                                                    "key": "http.route",
                                                    "value": {
                                                        "stringValue": "/api/users/{id}/orders"
                                                    }
                                                },
                                                {
                                                    "key": "user.email",
                                                    "value": {
                                                        "stringValue": "john.smith@example.org"
                                                    }
                                                }
                                            ]
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                ]
            }
        ]
    },
    "customPatterns": [
        {
            "name": "customer-ref",
            "pattern": "CUST-[0-9]{8}",
            "severity": "medium",
            "action": "hash"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("michiganpublictech/otel-pii-leak-detector").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "telemetry": {
        "resourceSpans": [{
                "resource": { "attributes": [
                        {
                            "key": "service.name",
                            "value": { "stringValue": "checkout" },
                        },
                        {
                            "key": "deployment.environment.name",
                            "value": { "stringValue": "demo" },
                        },
                    ] },
                "scopeSpans": [{
                        "scope": {
                            "name": "io.opentelemetry.http",
                            "version": "2.9.0",
                        },
                        "spans": [
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "eee19b7ec3c1b174",
                                "name": "GET /api/users/jane.doe@example.com/orders",
                                "kind": 2,
                                "attributes": [
                                    {
                                        "key": "http.request.method",
                                        "value": { "stringValue": "GET" },
                                    },
                                    {
                                        "key": "url.full",
                                        "value": { "stringValue": "https://shop.example.com/api/users/jane.doe@example.com/orders?api_key=demo-k3y-0f-fake-shop-9921&page=2" },
                                    },
                                    {
                                        "key": "http.request.header.authorization",
                                        "value": { "arrayValue": { "values": [{ "stringValue": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkphbmUgRG9lIiwiZW1haWwiOiJqYW5lLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMH0.ZGVtby1zaWduYXR1cmUtbm90LXJlYWw" }] } },
                                    },
                                    {
                                        "key": "client.address",
                                        "value": { "stringValue": "203.0.113.42" },
                                    },
                                    {
                                        "key": "user.id",
                                        "value": { "stringValue": "user-42" },
                                    },
                                    {
                                        "key": "app.customer_ref",
                                        "value": { "stringValue": "CUST-00012345" },
                                    },
                                    {
                                        "key": "geo.location.lat",
                                        "value": { "doubleValue": 42.3314 },
                                    },
                                    {
                                        "key": "geo.location.lon",
                                        "value": { "doubleValue": -83.0458 },
                                    },
                                ],
                                "events": [{
                                        "name": "exception",
                                        "attributes": [
                                            {
                                                "key": "exception.type",
                                                "value": { "stringValue": "OperationalError" },
                                            },
                                            {
                                                "key": "exception.message",
                                                "value": { "stringValue": "could not connect to postgres://orders_app:Sup3rS3cretPw@db.internal:5432/orders" },
                                            },
                                        ],
                                    }],
                                "status": {
                                    "code": 2,
                                    "message": "upstream payment call failed",
                                },
                            },
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "aaa19b7ec3c1b175",
                                "parentSpanId": "eee19b7ec3c1b174",
                                "name": "SELECT orders",
                                "kind": 3,
                                "attributes": [
                                    {
                                        "key": "db.system",
                                        "value": { "stringValue": "postgresql" },
                                    },
                                    {
                                        "key": "db.query.text",
                                        "value": { "stringValue": "SELECT * FROM customers WHERE email = 'jane.doe@example.com' AND phone = '+1 202-555-0143'" },
                                    },
                                ],
                            },
                        ],
                    }],
            }],
        "resourceLogs": [{
                "resource": { "attributes": [
                        {
                            "key": "service.name",
                            "value": { "stringValue": "payments" },
                        },
                        {
                            "key": "deployment.environment.name",
                            "value": { "stringValue": "demo" },
                        },
                    ] },
                "scopeLogs": [{
                        "scope": { "name": "payments.logger" },
                        "logRecords": [
                            {
                                "severityText": "INFO",
                                "body": { "stringValue": "Loaded AWS credentials AKIAIOSFODNN7EXAMPLE from environment" },
                                "attributes": [
                                    {
                                        "key": "payment.card_number",
                                        "value": { "stringValue": "4111 1111 1111 1111" },
                                    },
                                    {
                                        "key": "payment.iban",
                                        "value": { "stringValue": "GB82 WEST 1234 5698 7654 32" },
                                    },
                                ],
                            },
                            {
                                "severityText": "WARN",
                                "body": { "kvlistValue": { "values": [
                                            {
                                                "key": "event",
                                                "value": { "stringValue": "patient_lookup" },
                                            },
                                            {
                                                "key": "patient",
                                                "value": { "kvlistValue": { "values": [
                                                            {
                                                                "key": "mrn",
                                                                "value": { "stringValue": "MRN-00482913" },
                                                            },
                                                            {
                                                                "key": "ssn",
                                                                "value": { "stringValue": "078-05-1120" },
                                                            },
                                                            {
                                                                "key": "dob",
                                                                "value": { "stringValue": "1985-03-12" },
                                                            },
                                                        ] } },
                                            },
                                            {
                                                "key": "note",
                                                "value": { "stringValue": "Patient diagnosis E11.9, prescribed metformin" },
                                            },
                                        ] } },
                                "attributes": [],
                            },
                        ],
                    }],
            }],
        "resourceMetrics": [{
                "resource": { "attributes": [
                        {
                            "key": "service.name",
                            "value": { "stringValue": "checkout" },
                        },
                        {
                            "key": "deployment.environment.name",
                            "value": { "stringValue": "demo" },
                        },
                    ] },
                "scopeMetrics": [{
                        "scope": { "name": "io.opentelemetry.http" },
                        "metrics": [{
                                "name": "http.server.request.duration",
                                "unit": "s",
                                "description": "Duration of HTTP server requests.",
                                "histogram": {
                                    "aggregationTemporality": 2,
                                    "dataPoints": [{
                                            "count": "3",
                                            "sum": 0.42,
                                            "attributes": [
                                                {
                                                    "key": "http.route",
                                                    "value": { "stringValue": "/api/users/{id}/orders" },
                                                },
                                                {
                                                    "key": "user.email",
                                                    "value": { "stringValue": "john.smith@example.org" },
                                                },
                                            ],
                                        }],
                                },
                            }],
                    }],
            }],
    },
    "customPatterns": [{
            "name": "customer-ref",
            "pattern": "CUST-[0-9]{8}",
            "severity": "medium",
            "action": "hash",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("michiganpublictech/otel-pii-leak-detector").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "telemetry": {
    "resourceSpans": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            },
            {
              "key": "deployment.environment.name",
              "value": {
                "stringValue": "demo"
              }
            }
          ]
        },
        "scopeSpans": [
          {
            "scope": {
              "name": "io.opentelemetry.http",
              "version": "2.9.0"
            },
            "spans": [
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "eee19b7ec3c1b174",
                "name": "GET /api/users/jane.doe@example.com/orders",
                "kind": 2,
                "attributes": [
                  {
                    "key": "http.request.method",
                    "value": {
                      "stringValue": "GET"
                    }
                  },
                  {
                    "key": "url.full",
                    "value": {
                      "stringValue": "https://shop.example.com/api/users/jane.doe@example.com/orders?api_key=demo-k3y-0f-fake-shop-9921&page=2"
                    }
                  },
                  {
                    "key": "http.request.header.authorization",
                    "value": {
                      "arrayValue": {
                        "values": [
                          {
                            "stringValue": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkphbmUgRG9lIiwiZW1haWwiOiJqYW5lLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMH0.ZGVtby1zaWduYXR1cmUtbm90LXJlYWw"
                          }
                        ]
                      }
                    }
                  },
                  {
                    "key": "client.address",
                    "value": {
                      "stringValue": "203.0.113.42"
                    }
                  },
                  {
                    "key": "user.id",
                    "value": {
                      "stringValue": "user-42"
                    }
                  },
                  {
                    "key": "app.customer_ref",
                    "value": {
                      "stringValue": "CUST-00012345"
                    }
                  },
                  {
                    "key": "geo.location.lat",
                    "value": {
                      "doubleValue": 42.3314
                    }
                  },
                  {
                    "key": "geo.location.lon",
                    "value": {
                      "doubleValue": -83.0458
                    }
                  }
                ],
                "events": [
                  {
                    "name": "exception",
                    "attributes": [
                      {
                        "key": "exception.type",
                        "value": {
                          "stringValue": "OperationalError"
                        }
                      },
                      {
                        "key": "exception.message",
                        "value": {
                          "stringValue": "could not connect to postgres://orders_app:Sup3rS3cretPw@db.internal:5432/orders"
                        }
                      }
                    ]
                  }
                ],
                "status": {
                  "code": 2,
                  "message": "upstream payment call failed"
                }
              },
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "aaa19b7ec3c1b175",
                "parentSpanId": "eee19b7ec3c1b174",
                "name": "SELECT orders",
                "kind": 3,
                "attributes": [
                  {
                    "key": "db.system",
                    "value": {
                      "stringValue": "postgresql"
                    }
                  },
                  {
                    "key": "db.query.text",
                    "value": {
                      "stringValue": "SELECT * FROM customers WHERE email = '\''jane.doe@example.com'\'' AND phone = '\''+1 202-555-0143'\''"
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    ],
    "resourceLogs": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "payments"
              }
            },
            {
              "key": "deployment.environment.name",
              "value": {
                "stringValue": "demo"
              }
            }
          ]
        },
        "scopeLogs": [
          {
            "scope": {
              "name": "payments.logger"
            },
            "logRecords": [
              {
                "severityText": "INFO",
                "body": {
                  "stringValue": "Loaded AWS credentials AKIAIOSFODNN7EXAMPLE from environment"
                },
                "attributes": [
                  {
                    "key": "payment.card_number",
                    "value": {
                      "stringValue": "4111 1111 1111 1111"
                    }
                  },
                  {
                    "key": "payment.iban",
                    "value": {
                      "stringValue": "GB82 WEST 1234 5698 7654 32"
                    }
                  }
                ]
              },
              {
                "severityText": "WARN",
                "body": {
                  "kvlistValue": {
                    "values": [
                      {
                        "key": "event",
                        "value": {
                          "stringValue": "patient_lookup"
                        }
                      },
                      {
                        "key": "patient",
                        "value": {
                          "kvlistValue": {
                            "values": [
                              {
                                "key": "mrn",
                                "value": {
                                  "stringValue": "MRN-00482913"
                                }
                              },
                              {
                                "key": "ssn",
                                "value": {
                                  "stringValue": "078-05-1120"
                                }
                              },
                              {
                                "key": "dob",
                                "value": {
                                  "stringValue": "1985-03-12"
                                }
                              }
                            ]
                          }
                        }
                      },
                      {
                        "key": "note",
                        "value": {
                          "stringValue": "Patient diagnosis E11.9, prescribed metformin"
                        }
                      }
                    ]
                  }
                },
                "attributes": []
              }
            ]
          }
        ]
      }
    ],
    "resourceMetrics": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            },
            {
              "key": "deployment.environment.name",
              "value": {
                "stringValue": "demo"
              }
            }
          ]
        },
        "scopeMetrics": [
          {
            "scope": {
              "name": "io.opentelemetry.http"
            },
            "metrics": [
              {
                "name": "http.server.request.duration",
                "unit": "s",
                "description": "Duration of HTTP server requests.",
                "histogram": {
                  "aggregationTemporality": 2,
                  "dataPoints": [
                    {
                      "count": "3",
                      "sum": 0.42,
                      "attributes": [
                        {
                          "key": "http.route",
                          "value": {
                            "stringValue": "/api/users/{id}/orders"
                          }
                        },
                        {
                          "key": "user.email",
                          "value": {
                            "stringValue": "john.smith@example.org"
                          }
                        }
                      ]
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "customPatterns": [
    {
      "name": "customer-ref",
      "pattern": "CUST-[0-9]{8}",
      "severity": "medium",
      "action": "hash"
    }
  ]
}' |
apify call michiganpublictech/otel-pii-leak-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,michiganpublictech/otel-pii-leak-detector"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/FWjhyWQbjMNaJ3oGP/builds/nevuoNG3LdK0aXCAp/openapi.json
