---
title: "Telemetry and signals"
description: "How MainSDK logs, traces and audits on open standards, what is redacted, and what leaves your app and when."
section: "architecture"
status: "Building for 0.2"
spec: "log-telemetry"
canonical: "https://mainsdk.dev/docs/architecture/telemetry-and-signals"
updated: "2026-10-28"
---

# Telemetry and signals

> How MainSDK logs, traces and audits on open standards, what is redacted, and what leaves your app and when.

Status: Building for 0.2 · Canonical: https://mainsdk.dev/docs/architecture/telemetry-and-signals · Updated: 2026-10-28

Every app gets structured logs, traces and an append-only audit log from its first run, free and local. Redaction happens in your process before anything is written or exported. Nothing reaches MainSDK Cloud until you log in and opt in.

## Open standards

| Standard | Used for |
| --- | --- |
| OpenTelemetry logs data model | Every log record: severity, body, attributes, resource |
| OpenTelemetry semantic conventions | Where it happened (`code.function.name`, `code.file.path`, `code.line.number`) and what failed (`exception.*`) |
| W3C Trace Context | `traceparent` links an error to the request, job and change that caused it |
| RFC 9457 problem details | Every API error, plus our `code`, `fix`, `retryable`, `trace_id` and `rule` |
| RFC 3339, UTC | Every timestamp |
| OCSF | Audit and security events, read as-is by security tools |

Event ids are UUIDv7.

## Where telemetry goes

```mermaid
flowchart LR
  app["Your app<br/>log · spans · decisions · audit"] --> red["Redaction<br/>in process"]
  red --> term["terminal<br/>default"]
  red --> local[(".mainsdk/events/<br/>7 days, git-ignored")]
  red --> buf["Bounded buffer<br/>async, never blocks"]
  buf --> otel["Your OTLP endpoint<br/>Datadog, Grafana, Honeycomb …"]
  buf -. "only after mainsdk login<br/>and opt-in (phase 2)" .-> cloud["MainSDK Cloud<br/>your region"]
```

```yaml title="main.yaml"
telemetry:
  to: [terminal, otel, mainsdk]     # any mix; default [terminal]
  otel: { endpoint: https://otel.pinecart.dev:4318, headers: { authorization: secret:OTEL_TOKEN } }
  sample: 0.2                       # traces only
  budget: 2M                        # events a month sent to MainSDK Cloud
```

`MAINSDK_TELEMETRY=1` is the same as adding `mainsdk` to `telemetry.to`, and it still needs `mainsdk login`. Without login, no connection is made to any MainSDK host and the CLI says `log in to send telemetry`. Logout stops sending before the next batch.

## What is redacted, before anything leaves

- **Labelled values:** fields labelled `personal`, `sensitive`, `child` or `phi` become `[redacted:personal]` and so on. `pan` is never stored at all.
- **Secrets:** anything resolved from a `secret:` reference, and known key formats such as `msk_live_` and `msk_test_`.
- **Never collected by default:** request and response bodies, cookies, and the `authorization`, `cookie` and `set-cookie` headers.
- **User ids:** pseudonymised with a keyed hash per project before export.
- **IP addresses:** trimmed, IPv4 to /24 and IPv6 to /48. `203.0.113.77` is exported as `203.0.113.0`.

## The telemetry contract

`telemetry@1` lists every field we collect, its purpose and its redaction rule, published as JSON Schema. A field not in the contract is not exported: the exporter drops it and warns locally with `MAIN-LOG-0005`.

## Sampling never touches enforcement

`sample` applies to traces only. Errors, policy decisions and audit records are never sampled. Over a budget, we sample what we store, never what we enforce.

## The audit log

```mermaid
flowchart LR
  r1["record n-1<br/>hash h1"] --> r2["record n<br/>prev: h1, hash h2"] --> r3["record n+1<br/>prev: h2, hash h3"]
```

- Append-only and tamper-evident: each record carries the hash of the previous one. `log.audit.verify()` detects any edit, deletion or reorder and returns where the chain broke.
- Records are OCSF. Every policy decision is written with its policy version, for example as an OCSF Authorization event with actor, action, resource and decision.
- Never dropped silently: if its sink is unavailable, it buffers to local disk and reports `MAIN-LOG-0010`.

Still open: where the audit log lives in production by default, and whether chain checkpoints are signed, and with which key.

## When the exporter is down

Export is asynchronous, buffered and bounded. When the endpoint is unreachable, telemetry is dropped with a `telemetry.dropped` counter and requests keep their speed. See [request lifecycle](https://mainsdk.dev/docs/architecture/request-lifecycle).

## Security signals

| What | Phase |
| --- | --- |
| Hosted search, retention and `mainsdk logs --follow --env prod` from our endpoint | 2 |
| AI traces in the OpenTelemetry GenAI conventions | 2 |
| SIEM streaming per customer organisation | 2 |
| Fleet Defend: signals from inside every app to the SIEM in OCSF, enriched with app, owner, environment and data labels; detections as Sigma rules mapped to MITRE ATT&CK | 4 (Fleet) |

Fleet Defend signals come from the places MainSDK already decides: the sign-in check, the owner-scope check, the rate limiter, the outbound call, the AI call and the agent action. For example, an outbound call to an unknown host that was refused maps to T1041 and T1195. Because the SDK reports what it decided, not raw traffic, the signals are few.

## Next

- [Cloud and regions](https://mainsdk.dev/docs/architecture/cloud-and-regions): where exported telemetry is stored.
- [Request lifecycle](https://mainsdk.dev/docs/architecture/request-lifecycle): how the buffer stays off the request path.
- [Security architecture](https://mainsdk.dev/docs/architecture/security-model): the telemetry threats and their controls.
