|
| 1 | +# Drain Processor |
| 2 | + |
| 3 | +| Status | | |
| 4 | +| ------------- |-----------| |
| 5 | +| Stability | [development]: logs | |
| 6 | +| Distributions | [contrib] | |
| 7 | +| Issues | [](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Aprocessor%2Fdrain) [](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Aprocessor%2Fdrain) | |
| 8 | + |
| 9 | +[development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development |
| 10 | +[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib |
| 11 | + |
| 12 | +The drain processor applies the [Drain log clustering algorithm](https://jiemingzhu.github.io/pub/pjhe_icws2017.pdf) to log records as they pass through the pipeline. For each record it derives a template string (e.g. `"user <*> logged in from <*>"`) and a numeric cluster ID, then attaches both as attributes on the record. |
| 13 | + |
| 14 | +This processor **annotates**; it does not filter. Use the [filter processor](../filterprocessor/README.md) downstream to act on the `log.record.template` attribute — for example, to drop entire classes of noisy logs by pattern. |
| 15 | + |
| 16 | +## How it works |
| 17 | + |
| 18 | +Drain builds a parse tree from the token structure of log lines. Lines with similar structure are grouped into a **cluster**, and a **template** is derived by replacing variable tokens with `<*>` wildcards. As more logs arrive the templates become more accurate and stable. |
| 19 | + |
| 20 | +Template IDs are numeric and local to each collector instance. They are not stable across restarts unless the tree is pre-seeded with known templates (see [Seeding](#seeding)). Use the template **string** (not the ID) for persistent filtering rules. |
| 21 | + |
| 22 | +## Configuration |
| 23 | + |
| 24 | +```yaml |
| 25 | +processors: |
| 26 | + drain: |
| 27 | + # Drain parse tree parameters |
| 28 | + log_cluster_depth: 4 # default: 4 (minimum: 3) |
| 29 | + sim_threshold: 0.4 # default: 0.4, range [0.0, 1.0] |
| 30 | + max_children: 100 # default: 100 |
| 31 | + max_clusters: 0 # default: 0 (unlimited, LRU eviction when > 0) |
| 32 | + extra_delimiters: [] # default: [] (extra token delimiters beyond whitespace) |
| 33 | + |
| 34 | + # Body extraction |
| 35 | + body_field: "" # default: "" (use full body string) |
| 36 | + |
| 37 | + # Output attribute names |
| 38 | + template_attribute: "log.record.template" # default |
| 39 | + template_id_attribute: "log.record.template.id" # default |
| 40 | + |
| 41 | + # Seeding (optional) |
| 42 | + seed_templates: [] |
| 43 | + seed_logs: [] |
| 44 | + |
| 45 | + # Warmup mode |
| 46 | + warmup_mode: passthrough # default: "passthrough" | "buffer" |
| 47 | + warmup_min_clusters: 10 # default: 10 (only used when warmup_mode: buffer) |
| 48 | + warmup_buffer_max_logs: 10000 # default: 10000 (only used when warmup_mode: buffer) |
| 49 | +``` |
| 50 | +
|
| 51 | +### Parameters |
| 52 | +
|
| 53 | +| Field | Type | Default | Description | |
| 54 | +|-------|------|---------|-------------| |
| 55 | +| `log_cluster_depth` | int | `4` | Max depth of the Drain parse tree. Higher values produce more specific templates. Minimum: 3. | |
| 56 | +| `sim_threshold` | float | `0.4` | Similarity threshold in [0.0, 1.0]. Lines below this threshold create a new cluster rather than merging with an existing one. | |
| 57 | +| `max_children` | int | `100` | Maximum children per parse tree node. | |
| 58 | +| `max_clusters` | int | `0` | Maximum clusters tracked. When exceeded, the least-recently-used cluster is evicted. `0` means unlimited. | |
| 59 | +| `extra_delimiters` | []string | `[]` | Additional token delimiters beyond whitespace (e.g. `[",", ":"]`). | |
| 60 | +| `body_field` | string | `""` | If set, and the log body is a structured map, the value of this top-level key is used as the text to template instead of the full body. | |
| 61 | +| `template_attribute` | string | `"log.record.template"` | Attribute key written with the derived template string. | |
| 62 | +| `template_id_attribute` | string | `"log.record.template.id"` | Attribute key written with the numeric cluster ID. | |
| 63 | +| `seed_templates` | []string | `[]` | Template strings to pre-load at startup (see [Seeding](#seeding)). | |
| 64 | +| `seed_logs` | []string | `[]` | Raw example log lines to train on at startup (see [Seeding](#seeding)). | |
| 65 | +| `warmup_mode` | string | `"passthrough"` | Controls behavior during the warmup period. `"passthrough"` (default) or `"buffer"` (see [Warmup mode](#warmup-mode)). | |
| 66 | +| `warmup_min_clusters` | int | `10` | Minimum distinct clusters before warmup ends. Only used when `warmup_mode: buffer`. | |
| 67 | +| `warmup_buffer_max_logs` | int | `10000` | Maximum records to buffer before flushing regardless of cluster count. Only used when `warmup_mode: buffer`. Must be > 0. | |
| 68 | + |
| 69 | +## Seeding |
| 70 | + |
| 71 | +Seeding pre-populates the Drain tree before any live logs arrive. This is the primary mechanism for stable template IDs across restarts. |
| 72 | + |
| 73 | +### `seed_templates` |
| 74 | + |
| 75 | +Provide known template strings directly. The processor trains on each entry at startup, establishing clusters for those patterns immediately. |
| 76 | + |
| 77 | +```yaml |
| 78 | +processors: |
| 79 | + drain: |
| 80 | + seed_templates: |
| 81 | + - "user <*> logged in from <*>" |
| 82 | + - "connected to <*>" |
| 83 | + - "heartbeat ping <*>" |
| 84 | +``` |
| 85 | + |
| 86 | +### `seed_logs` |
| 87 | + |
| 88 | +Provide raw example log lines. The processor trains on them at startup, letting Drain derive the templates itself. Useful when exact template strings are not known in advance. |
| 89 | + |
| 90 | +```yaml |
| 91 | +processors: |
| 92 | + drain: |
| 93 | + seed_logs: |
| 94 | + - "user alice logged in from 10.0.0.1" |
| 95 | + - "user bob logged in from 192.168.1.1" |
| 96 | + - "connected to 10.0.0.1" |
| 97 | +``` |
| 98 | + |
| 99 | +Empty and whitespace-only entries in both lists are silently skipped. |
| 100 | + |
| 101 | +> **Note on multi-instance deployments**: Each collector instance maintains its own independent Drain tree. Template IDs will differ between instances. Providing identical `seed_templates` across all instances produces consistent template **strings** (though IDs may still differ). Filtering rules should always match on the template string, not the ID. |
| 102 | + |
| 103 | +## Warmup mode |
| 104 | + |
| 105 | +### `passthrough` (default) |
| 106 | + |
| 107 | +Records are annotated and forwarded immediately from the first record. Early templates may be unstable (exact log lines rather than abstracted patterns) until enough similar lines have been observed. |
| 108 | + |
| 109 | +### `buffer` |
| 110 | + |
| 111 | +Records are held in memory until `warmup_min_clusters` distinct templates have been observed, at which point the buffer is flushed with annotations applied using the now-stable templates. If `warmup_buffer_max_logs` is reached before the cluster threshold, the buffer is flushed anyway. |
| 112 | + |
| 113 | +Use buffer mode when downstream consumers (e.g. a filter processor) must act on stable, wildcard-abstracted templates from the very first record. |
| 114 | + |
| 115 | +```yaml |
| 116 | +processors: |
| 117 | + drain: |
| 118 | + warmup_mode: buffer |
| 119 | + warmup_min_clusters: 20 |
| 120 | + warmup_buffer_max_logs: 5000 |
| 121 | +``` |
| 122 | + |
| 123 | +> **Memory note**: in buffer mode, all records are held in memory until flush. Size the buffer with `warmup_buffer_max_logs` according to your available memory and expected log volume during startup. |
| 124 | + |
| 125 | +## Output attributes |
| 126 | + |
| 127 | +By default the processor sets two attributes on each log record: |
| 128 | + |
| 129 | +| Attribute | Type | Example | Description | |
| 130 | +|-----------|------|---------|-------------| |
| 131 | +| `log.record.template` | string | `"user <*> logged in from <*>"` | The Drain-derived template string. Stable within an instance once the tree has warmed up. Use this for filtering rules. | |
| 132 | +| `log.record.template.id` | int | `3` | Numeric cluster ID. Unstable across restarts unless seeding is used. | |
| 133 | + |
| 134 | +Both attribute names are configurable via `template_attribute` and `template_id_attribute`. |
| 135 | + |
| 136 | +> **Semantic conventions**: `log.record.template` aligns with the proposed OTel attribute in [open-telemetry/semantic-conventions#1283](https://github.com/open-telemetry/semantic-conventions/issues/1283) and [#2064](https://github.com/open-telemetry/semantic-conventions/issues/2064). These names may be updated if a convention is formally adopted. |
| 137 | + |
| 138 | +## Example pipeline |
| 139 | + |
| 140 | +The following pipeline annotates logs with Drain templates and then drops known noisy patterns using the filter processor: |
| 141 | + |
| 142 | +```yaml |
| 143 | +processors: |
| 144 | + drain: |
| 145 | + log_cluster_depth: 4 |
| 146 | + sim_threshold: 0.4 |
| 147 | + max_clusters: 500 |
| 148 | + seed_templates: |
| 149 | + - "user <*> logged in from <*>" |
| 150 | + - "connected to <*>" |
| 151 | + - "heartbeat ping <*>" |
| 152 | + warmup_mode: buffer |
| 153 | + warmup_min_clusters: 20 |
| 154 | + warmup_buffer_max_logs: 5000 |
| 155 | +
|
| 156 | + filter/drop_noisy: |
| 157 | + error_mode: ignore |
| 158 | + logs: |
| 159 | + log_record: |
| 160 | + - attributes["log.record.template"] == "heartbeat ping <*>" |
| 161 | + - attributes["log.record.template"] == "connected to <*>" |
| 162 | +
|
| 163 | +service: |
| 164 | + pipelines: |
| 165 | + logs: |
| 166 | + receivers: [otlp] |
| 167 | + processors: [drain, filter/drop_noisy] |
| 168 | + exporters: [otlp] |
| 169 | +``` |
| 170 | + |
| 171 | +## `body_field` |
| 172 | + |
| 173 | +`body_field` is a convenience for pipelines where the log body is a structured map and you do not have full control over how upstream processors shape it. |
| 174 | + |
| 175 | +If you **do** control the pipeline, the preferred approach is a `move` operator in the filelog receiver (or equivalent) to promote the message field back to a plain string body before the drain processor sees the record: |
| 176 | + |
| 177 | +```yaml |
| 178 | +operators: |
| 179 | + - type: json_parser |
| 180 | + - type: move |
| 181 | + from: body.message |
| 182 | + to: body |
| 183 | +``` |
| 184 | + |
| 185 | +If you **cannot** do that — for example, logs arrive via OTLP already structured — set `body_field` to the map key whose value should be fed to Drain: |
| 186 | + |
| 187 | +```yaml |
| 188 | +processors: |
| 189 | + drain: |
| 190 | + body_field: "message" |
| 191 | +``` |
| 192 | + |
| 193 | +Given a log body `{"level": "info", "message": "user alice logged in from 10.0.0.1"}`, only the `message` value is fed to Drain. The full body is used unchanged if the field is absent or the body is not a map. |
| 194 | + |
| 195 | +> **Note**: `body_field` only supports a single top-level key. Full OTTL path expressions (e.g. `body["event"]["message"]`) are not supported and are noted as a future extension. |
| 196 | + |
| 197 | +## Future extensions |
| 198 | + |
| 199 | +- **Snapshot persistence**: save and restore the Drain tree state across restarts, eliminating the need for seeding. This requires serialization support and is tracked as a future improvement. |
| 200 | +- **OTTL body extraction**: support full OTTL path expressions for `body_field` instead of a single top-level key name. |
| 201 | +- **Multi-instance synchronisation**: optional shared snapshot file or gossip-based tree merging for consistent templates across horizontally scaled deployments. |
0 commit comments