|
| 1 | +# OtlpComputeLogManager |
| 2 | + |
| 3 | +Send Dagster op stdout/stderr to **any OpenTelemetry-compatible logs backend** via OTLP/HTTP (JSON). One wire protocol, many destinations: |
| 4 | + |
| 5 | +- **Splunk** via [Splunk Distribution of OpenTelemetry Collector](https://docs.splunk.com/observability/en/gdi/opentelemetry/opentelemetry.html) |
| 6 | +- **Datadog** OTel ingest (`https://otlp-intake.logs.datadoghq.com`) |
| 7 | +- **Honeycomb** (`https://api.honeycomb.io`) |
| 8 | +- **Sumo Logic** OTel |
| 9 | +- **Grafana Loki** via OTel Collector |
| 10 | +- **AWS CloudWatch** via OTel Collector |
| 11 | +- **New Relic** (`https://otlp.nr-data.net`) |
| 12 | +- Any OTel Collector → forwards to anything |
| 13 | + |
| 14 | +Compute log manager — sits at the Dagster **instance** layer (`dagster.yaml`), not the project layer. |
| 15 | + |
| 16 | +## Why OTLP vs the dedicated `SplunkComputeLogManager`? |
| 17 | + |
| 18 | +| | HEC direct (`splunk/`) | OTLP (`otlp/`, this manager) | |
| 19 | +|---|---|---| |
| 20 | +| Hops | Dagster → Splunk | Dagster → Collector → Splunk | |
| 21 | +| Setup | HEC token + endpoint | Operate an OTel Collector | |
| 22 | +| Reliability | Splunk down = events drop | Collector usually buffers + retries | |
| 23 | +| Vendor portability | Splunk-only | Splunk + Datadog + Honeycomb + Sumo + Loki + CloudWatch + ... | |
| 24 | +| Splunk's docs lean | "Still supported, mature" | "Modern recommended path" | |
| 25 | + |
| 26 | +Pick OTLP if you already run an OTel Collector for app traces/metrics (Dagster slots into the existing pipeline), or if you need vendor portability. Pick HEC if Splunk is your only observability backend and adding a Collector is unwelcome ops surface. |
| 27 | + |
| 28 | +## What lands at the backend |
| 29 | + |
| 30 | +One OTLP `LogRecord` per line of captured stdout/stderr: |
| 31 | + |
| 32 | +```json |
| 33 | +{ |
| 34 | + "resourceLogs": [{ |
| 35 | + "resource": { |
| 36 | + "attributes": [ |
| 37 | + {"key": "service.name", "value": {"stringValue": "dagster"}}, |
| 38 | + {"key": "service.instance.id", "value": {"stringValue": "prod-east"}} |
| 39 | + ] |
| 40 | + }, |
| 41 | + "scopeLogs": [{ |
| 42 | + "scope": {"name": "dagster_community_components.compute_log_managers.otlp"}, |
| 43 | + "logRecords": [ |
| 44 | + { |
| 45 | + "timeUnixNano": "1748534567000000000", |
| 46 | + "severityNumber": 9, |
| 47 | + "severityText": "INFO", |
| 48 | + "body": {"stringValue": "INFO - my asset processed 1234 rows"}, |
| 49 | + "attributes": [ |
| 50 | + {"key": "dagster.run_id", "value": {"stringValue": "abc-123-uuid"}}, |
| 51 | + {"key": "dagster.step_key", "value": {"stringValue": "process_orders"}}, |
| 52 | + {"key": "dagster.io_type", "value": {"stringValue": "stdout"}}, |
| 53 | + {"key": "dagster.partial", "value": {"stringValue": "false"}} |
| 54 | + ] |
| 55 | + } |
| 56 | + ] |
| 57 | + }] |
| 58 | + }] |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +Severity: stdout → 9 (INFO), stderr → 13 (WARN) by default. **Not ERROR** for stderr because Dagster doesn't distinguish app errors from operational stderr (most Python loggers write everything there). Override per-backend via `severity_stdout` / `severity_stderr` if your observability strategy treats stderr differently. |
| 63 | + |
| 64 | +## UI behavior |
| 65 | + |
| 66 | +Per step, the Dagster UI shows a "View logs →" button that opens the URL from your `display_url_template` with `{run_id}` / `{step_key}` / `{io_type}` / `{location}` / `{service}` substituted. If `display_url_template` is unset, no deep-link is rendered (UI shows the stub message only). |
| 67 | + |
| 68 | +Example templates per backend: |
| 69 | + |
| 70 | +```yaml |
| 71 | +# Honeycomb |
| 72 | +display_url_template: "https://ui.honeycomb.io/acme/datasets/dagster_compute_logs?query=dagster.run_id%3D{run_id}" |
| 73 | + |
| 74 | +# Datadog (logs query syntax) |
| 75 | +display_url_template: "https://app.datadoghq.com/logs?query=%40dagster.run_id%3A{run_id}" |
| 76 | + |
| 77 | +# Splunk Web (via OTel Collector) |
| 78 | +display_url_template: "https://splunk.acme.com:8000/en-US/app/search/search?q=search%20dagster.run_id%3D%22{run_id}%22" |
| 79 | + |
| 80 | +# Grafana Loki |
| 81 | +display_url_template: "https://grafana.acme.com/explore?left=%7B%22queries%22%3A%5B%7B%22expr%22%3A%22%7Bservice_name%3D%5C%22{service}%5C%22%7D%20%7C%3D%20%5C%22{run_id}%5C%22%22%7D%5D%7D" |
| 82 | +``` |
| 83 | +
|
| 84 | +## Setup |
| 85 | +
|
| 86 | +### 1. Stand up an OTel Collector (or use a hosted one) |
| 87 | +
|
| 88 | +Most observability vendors offer a hosted OTLP endpoint. Examples: |
| 89 | +
|
| 90 | +| Vendor | Endpoint | Headers | |
| 91 | +|---|---|---| |
| 92 | +| Honeycomb | `https://api.honeycomb.io` | `x-honeycomb-team`, `x-honeycomb-dataset` | |
| 93 | +| Datadog (US1) | `https://otlp-intake.logs.datadoghq.com` | `DD-API-KEY` | |
| 94 | +| New Relic (US) | `https://otlp.nr-data.net` | `api-key` | |
| 95 | +| Splunk Observability Cloud | `https://ingest.us0.signalfx.com/v2/log` (NB: SignalFx path) | `X-SF-Token` | |
| 96 | +| Splunk Enterprise via OTC | `http://your-otc:4318` (no auth typical) | (intra-cluster) | |
| 97 | + |
| 98 | +If you're running your own Collector, the receiver config looks like: |
| 99 | + |
| 100 | +```yaml |
| 101 | +# otel-collector-config.yaml |
| 102 | +receivers: |
| 103 | + otlp: |
| 104 | + protocols: |
| 105 | + http: |
| 106 | + endpoint: 0.0.0.0:4318 |
| 107 | +
|
| 108 | +exporters: |
| 109 | + splunk_hec: |
| 110 | + token: ${SPLUNK_HEC_TOKEN} |
| 111 | + endpoint: https://splunk.acme.com:8088/services/collector |
| 112 | +
|
| 113 | +service: |
| 114 | + pipelines: |
| 115 | + logs: |
| 116 | + receivers: [otlp] |
| 117 | + exporters: [splunk_hec] |
| 118 | +``` |
| 119 | + |
| 120 | +### 2. Add to `dagster.yaml` |
| 121 | + |
| 122 | +```yaml |
| 123 | +compute_logs: |
| 124 | + module: dagster_community_components.compute_log_managers.otlp |
| 125 | + class: OtlpComputeLogManager |
| 126 | + config: |
| 127 | + otlp_endpoint: http://otel-collector.svc:4318 |
| 128 | + service_name: dagster |
| 129 | + location_label: prod-east |
| 130 | + display_url_template: "https://splunk.acme.com:8000/en-US/app/search/search?q=search%20dagster.run_id%3D%22{run_id}%22" |
| 131 | + upload_interval: 30 |
| 132 | +``` |
| 133 | + |
| 134 | +Full annotated config in [`example_dagster.yaml`](example_dagster.yaml). |
| 135 | + |
| 136 | +### 3. Restart |
| 137 | + |
| 138 | +Daemon + webserver + any user-code containers pick up `dagster.yaml` at startup. |
| 139 | + |
| 140 | +## Config reference |
| 141 | + |
| 142 | +| Field | Type | Default | Description | |
| 143 | +|---|---|---|---| |
| 144 | +| `otlp_endpoint` | str (env) | — | Base URL. `/v1/logs` is appended. | |
| 145 | +| `otlp_headers` | dict[str, str] | `{}` | Headers added to every OTLP POST (vendor-specific auth). | |
| 146 | +| `service_name` | str (env) | `dagster` | OTel `service.name` resource attribute. | |
| 147 | +| `location_label` | str (env) | `default` | OTel `service.instance.id` resource attribute. | |
| 148 | +| `display_url_template` | str (env) | none | URL template for the Dagster UI deep-link. Supports `{run_id}` / `{step_key}` / `{io_type}` / `{location}` / `{service}` placeholders. | |
| 149 | +| `severity_stdout` | int | `9` (INFO) | OTel severity number for stdout lines. | |
| 150 | +| `severity_stderr` | int | `13` (WARN) | OTel severity number for stderr lines. | |
| 151 | +| `verify_ssl` | bool | `true` | TLS cert verification. | |
| 152 | +| `batch_size` | int | `100` | LogRecords per OTLP POST. | |
| 153 | +| `request_timeout_seconds` | int | `30` | HTTP timeout per POST. | |
| 154 | +| `skip_empty_files` | bool | `false` | Skip uploads when local file is empty. | |
| 155 | +| `local_dir` | str (env) | system temp | Local capture directory. | |
| 156 | +| `upload_interval` | int \| null | `null` | Partial-upload interval in seconds. `null` = upload on step finish only. | |
| 157 | + |
| 158 | +## Sending to OTLP AND Dagster+ |
| 159 | + |
| 160 | +Wrap in [`TeeComputeLogManager`](../tee/): |
| 161 | + |
| 162 | +```yaml |
| 163 | +compute_logs: |
| 164 | + module: dagster_community_components.compute_log_managers.tee |
| 165 | + class: TeeComputeLogManager |
| 166 | + config: |
| 167 | + local_dir: /tmp/dagster_compute_logs |
| 168 | + display_manager_index: 0 # OTLP URL surfaces in UI |
| 169 | + managers: |
| 170 | + - module: dagster_community_components.compute_log_managers.otlp |
| 171 | + class: OtlpComputeLogManager |
| 172 | + config: |
| 173 | + otlp_endpoint: http://otel-collector.svc:4318 |
| 174 | + display_url_template: "..." |
| 175 | + - module: dagster_cloud.storage.compute_logs |
| 176 | + class: CloudComputeLogManager |
| 177 | + config: {} |
| 178 | +``` |
| 179 | + |
| 180 | +## Cost / performance notes |
| 181 | + |
| 182 | +- **Latency**: OTLP/HTTP JSON over HTTPS — single round-trip per batch, ~5–50ms typical to a local Collector, ~50–200ms to hosted vendors. Set `batch_size` higher (e.g. 500) for chatty workloads to reduce HTTP overhead. |
| 183 | +- **Reliability**: Collector failure modes vary. Most Collectors buffer locally and retry to the backend, so transient backend outages are absorbed. If your Collector is misconfigured or down, OTLP POSTs raise and Dagster's CLM machinery logs the error — the local file stays around but isn't auto-retried by this manager. |
| 184 | +- **Cardinality**: each log line has the same low-cardinality attributes (`dagster.run_id`, `dagster.step_key`, `dagster.io_type`). Won't blow up tag-based pricing systems like Datadog or Honeycomb at sensible volumes. |
| 185 | +- **Severity tagging**: stderr defaults to WARN, not ERROR. If your observability strategy treats stderr=ERROR (PagerDuty rules, etc.), bump `severity_stderr: 17` — but be aware Dagster + Python's `logging` write a lot of non-error content to stderr by default. |
| 186 | + |
| 187 | +## See also |
| 188 | + |
| 189 | +- [`splunk/`](../splunk/) — Splunk HEC direct (no Collector required, single hop) |
| 190 | +- [`tee/`](../tee/) — compose OTLP with Dagster+ or any other CLM |
| 191 | +- [OTLP/HTTP spec — logs](https://opentelemetry.io/docs/specs/otlp/#otlphttp-logs-binary-protocol) |
| 192 | +- [Splunk Distribution of OpenTelemetry Collector](https://docs.splunk.com/observability/en/gdi/opentelemetry/opentelemetry.html) |
| 193 | +- `dataframe_to_otlp_logs` (in `assets/sinks/`) — distinct: ships data-plane DataFrames as OTLP logs, not compute logs |
| 194 | +- `dagster_failures_to_otlp_sensor` (in `sensors/`) — distinct: ships Dagster run-failure events to OTLP, not compute logs |
0 commit comments