Skip to content

Commit b432018

Browse files
authored
feat: support otlp logs in the proxy (#33)
1 parent 284055d commit b432018

26 files changed

Lines changed: 3725 additions & 38 deletions

File tree

‎.github/workflows/release.yaml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ on:
1717
type: choice
1818
options:
1919
- datadog
20+
- otlp
2021

2122
env:
2223
REGISTRY: ghcr.io
@@ -35,7 +36,7 @@ jobs:
3536
strategy:
3637
fail-fast: false
3738
matrix:
38-
distribution: ${{ github.event_name == 'workflow_dispatch' && fromJson(format('["{0}"]', inputs.distribution)) || fromJson('["datadog"]') }}
39+
distribution: ${{ github.event_name == 'workflow_dispatch' && fromJson(format('["{0}"]', inputs.distribution)) || fromJson('["datadog", "otlp"]') }}
3940
arch:
4041
- platform: linux/amd64
4142
runner: ubuntu-latest
@@ -116,7 +117,7 @@ jobs:
116117
strategy:
117118
fail-fast: false
118119
matrix:
119-
distribution: ${{ github.event_name == 'workflow_dispatch' && fromJson(format('["{0}"]', inputs.distribution)) || fromJson('["datadog"]') }}
120+
distribution: ${{ github.event_name == 'workflow_dispatch' && fromJson(format('["{0}"]', inputs.distribution)) || fromJson('["datadog", "otlp"]') }}
120121
steps:
121122
- name: Prepare
122123
run: |

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Multi-stage Dockerfile for Tero Edge distributions
22
# Build specific distributions with: docker build --build-arg DISTRIBUTION=<name> -t tero-edge-<name> .
3-
# Available distributions: datadog, otlp (future), full
3+
# Available distributions: datadog, otlp
44
#
55
# =============================================================================
66
# Build stage

‎README.md‎

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
# Tero Edge
22

3-
Tero Edge is a lightweight, high-performance telemetry proxy that enables efficient telemetry processing via a set of policies. The repository is structured as modular packages that can be composed to enable multiple use cases and scenarios.
3+
Tero Edge is a lightweight, high-performance telemetry proxy that enables efficient telemetry processing via a set of policies. The repository is structured as modular packages that can be composed to enable multiple use cases and scenarios. The proxy demonstrates how to implement the policy spec described in [this OTEP](https://github.com/open-telemetry/opentelemetry-specification/pull/4738). This project is not meant to replace the opentelemetry collector but rather work in tandem with it, providing a lightweight alternative for solely applying policies. It's expected that a follow up to this project will be a collector processor Policy implementation.
44

55
## Current Configurations
66

77
1. **Edge data proxy** - Receives data in a gateway or sidecar configuration, processes the data through its policy engine, and forwards it to the next destination.
88

9+
2. **OTLP (OpenTelemetry) proxy** - Receives OpenTelemetry logs via the `/v1/logs` endpoint, applies policy-based filtering (DROP/KEEP), and forwards to OTLP-compatible backends.
10+
11+
2. **Datadog proxy** - Receives OpenTelemetry logs via the `/api/v2/logs` endpoint, applies policy-based filtering (DROP/KEEP), and forwards to Datadog.
12+
913
## Repository Structure
1014

1115
```
1216
src/
1317
├── main.zig # Default distribution entry point
1418
├── datadog_main.zig # Datadog-focused distribution entry point
19+
├── otlp_main.zig # OTLP-focused distribution entry point
1520
├── root.zig # Library root (public API exports)
1621
│
1722
├── policy/ # Policy management package
@@ -105,6 +110,7 @@ Protocol-specific request processing modules that plug into the proxy infrastruc
105110
- `ProxyModule` - Vtable interface for request processing
106111
- `ModuleRegistration` - Module configuration (routes, upstream, etc.)
107112
- `DatadogModule` - Datadog log ingestion with policy-based filtering
113+
- `OtlpModule` - OpenTelemetry log ingestion with policy-based filtering
108114
- `PassthroughModule` - No-op passthrough for unhandled routes
109115

110116
**Integration:**
@@ -135,7 +141,24 @@ const registrations = [_]modules.ModuleRegistration{
135141
};
136142
```
137143

138-
**Dependencies:** `policy/` (for DatadogModule), `observability/`
144+
**Dependencies:** `policy/` (for DatadogModule, OtlpModule), `observability/`
145+
146+
#### OtlpModule
147+
148+
The OTLP module handles OpenTelemetry log ingestion with policy-based filtering. It processes the standard OTLP JSON format (`ExportLogsServiceRequest`).
149+
150+
**Supported filter match cases:**
151+
- `log_body` - Match against log message body
152+
- `log_severity_text` - Match against severity text (INFO, DEBUG, ERROR, etc.)
153+
- `log_severity_number` - Match against severity number (1-24)
154+
- `log_attribute` - Match against log record attributes
155+
- `resource_attribute` - Match against resource attributes (e.g., `service.name`)
156+
- `scope_name` - Match against instrumentation scope name
157+
- `scope_version` - Match against instrumentation scope version
158+
- `scope_attribute` - Match against scope attributes
159+
160+
**Routes:**
161+
- `POST /v1/logs` - OTLP log ingestion endpoint
139162

140163
---
141164

@@ -255,6 +278,19 @@ Focused distribution for Datadog log ingestion with:
255278

256279
Build: `zig build` (default) or `zig build datadog`
257280

281+
### OTLP (`otlp_main.zig`)
282+
Focused distribution for OpenTelemetry Protocol (OTLP) log ingestion with:
283+
- Handles `/v1/logs` endpoint (OTLP JSON format)
284+
- Policy-based log filtering (DROP/KEEP) on log body, severity, attributes
285+
- Support for resource attributes, scope attributes, and log attributes
286+
- Fail-open behavior (errors pass logs through unchanged)
287+
- Lock-free policy updates via atomic snapshots
288+
- Graceful shutdown with signal handling
289+
- Compatible with any OTLP-receiving backend (Datadog, Jaeger, etc.)
290+
291+
Build: `zig build otlp`
292+
Run: `zig build run-otlp` or `./zig-out/bin/edge-otlp [config-file]`
293+
258294
## Building
259295

260296
```bash
@@ -266,16 +302,53 @@ zig build test
266302

267303
# Build specific distribution
268304
zig build datadog
305+
zig build otlp
306+
307+
# Run specific distribution
308+
zig build run-datadog
309+
zig build run-otlp
269310
```
270311

271312
## Configuration
272313

273-
See `config.json` for example configuration. Key settings:
314+
See `config.json` for the Datadog distribution or `config-otlp.json` for the OTLP distribution.
315+
316+
### Key settings:
274317
- `listen_address` / `listen_port` - Server bind address
275318
- `upstream_url` - Default upstream destination
319+
- `workspace_id` - Workspace identifier for policy sync
320+
- `log_level` - Logging level (trace, debug, info, warn, err)
276321
- `policy_providers` - List of policy sources (file/http)
277322
- `max_body_size` - Request/response body limits
278323

324+
### Example OTLP Configuration (`config-otlp.json`):
325+
326+
```json
327+
{
328+
"listen_address": "127.0.0.1",
329+
"listen_port": 8080,
330+
"upstream_url": "https://otlp.us5.datadoghq.com",
331+
"workspace_id": "your-workspace-id",
332+
"log_level": "info",
333+
"max_body_size": 1048576,
334+
"policy_providers": [
335+
{
336+
"id": "file",
337+
"type": "file",
338+
"path": "policies.json"
339+
},
340+
{
341+
"id": "http",
342+
"type": "http",
343+
"url": "http://localhost:9090/v1/policy/sync"
344+
}
345+
]
346+
}
347+
```
348+
349+
### Environment Variables:
350+
- `TERO_LOG_LEVEL` - Override log level (trace, debug, info, warn, err)
351+
279352
## Design Principles
280353

281354
1. **Data-Oriented Design** - Optimize for cache coherency and memory access patterns

‎Taskfile.yml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ tasks:
226226
update:protos:
227227
desc: "Update proto files"
228228
cmds:
229-
- buf export buf.build/tero/policy -o proto/policy
229+
- buf export buf.build/tero/policy -o proto
230+
- buf export buf.build/opentelemetry/opentelemetry -o proto
230231
- task: build:protos
231232

232233
# =============================================================================
@@ -268,6 +269,8 @@ tasks:
268269
cmds:
269270
- task: build:container
270271
vars: { DIST: "datadog" }
272+
- task: build:container
273+
vars: { DIST: "otlp" }
271274

272275
push:container:
273276
desc: "🚀 Push container to GCP Artifact Registry (DIST=datadog|otlp|full)"
@@ -301,6 +304,8 @@ tasks:
301304
cmds:
302305
- task: push:container
303306
vars: { DIST: "datadog" }
307+
- task: push:container
308+
vars: { DIST: "otlp" }
304309

305310
release:
306311
desc: "🎯 Build and push all containers (for CI)"

‎build.zig‎

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -119,36 +119,48 @@ pub fn build(b: *std.Build) void {
119119
b.installArtifact(exe);
120120

121121
// ==========================================================================
122-
// Datadog Distribution
122+
// Distribution Builds
123123
// ==========================================================================
124-
// A focused distribution for Datadog log ingestion with filtering.
125-
// Build with: zig build datadog
126-
// Run with: zig build run-datadog
127-
const datadog_exe = b.addExecutable(.{
128-
.name = "edge-datadog",
129-
.root_module = b.createModule(.{
130-
.root_source_file = b.path("src/datadog_main.zig"),
131-
.target = target,
132-
.optimize = optimize,
133-
}),
134-
});
135-
datadog_exe.root_module.addImport("httpz", httpz.module("httpz"));
136-
datadog_exe.root_module.addImport("protobuf", protobuf_dep.module("protobuf"));
137-
datadog_exe.root_module.addImport("proto", proto_mod);
138-
datadog_exe.root_module.link_libc = true;
139-
datadog_exe.root_module.linkSystemLibrary("z", .{});
140-
datadog_exe.root_module.linkSystemLibrary("zstd", .{});
141-
datadog_exe.root_module.linkSystemLibrary("hs", .{});
142-
143-
const datadog_step = b.step("datadog", "Build the Datadog distribution");
144-
datadog_step.dependOn(&b.addInstallArtifact(datadog_exe, .{}).step);
145-
146-
const run_datadog_step = b.step("run-datadog", "Run the Datadog distribution");
147-
const run_datadog_cmd = b.addRunArtifact(datadog_exe);
148-
run_datadog_step.dependOn(&run_datadog_cmd.step);
149-
run_datadog_cmd.step.dependOn(b.getInstallStep());
150-
if (b.args) |args| {
151-
run_datadog_cmd.addArgs(args);
124+
// Each distribution is a focused edge proxy for a specific backend.
125+
// Build with: zig build <distribution>
126+
// Run with: zig build run-<distribution>
127+
128+
const distributions = .{
129+
.{ "datadog", "src/datadog_main.zig", "Datadog log ingestion" },
130+
.{ "otlp", "src/otlp_main.zig", "OpenTelemetry (OTLP) log ingestion" },
131+
};
132+
133+
inline for (distributions) |dist| {
134+
const name = dist[0];
135+
const source = dist[1];
136+
const desc = dist[2];
137+
138+
const dist_exe = b.addExecutable(.{
139+
.name = "edge-" ++ name,
140+
.root_module = b.createModule(.{
141+
.root_source_file = b.path(source),
142+
.target = target,
143+
.optimize = optimize,
144+
}),
145+
});
146+
dist_exe.root_module.addImport("httpz", httpz.module("httpz"));
147+
dist_exe.root_module.addImport("protobuf", protobuf_dep.module("protobuf"));
148+
dist_exe.root_module.addImport("proto", proto_mod);
149+
dist_exe.root_module.link_libc = true;
150+
dist_exe.root_module.linkSystemLibrary("z", .{});
151+
dist_exe.root_module.linkSystemLibrary("zstd", .{});
152+
dist_exe.root_module.linkSystemLibrary("hs", .{});
153+
154+
const dist_step = b.step(name, "Build the " ++ name ++ " distribution (" ++ desc ++ ")");
155+
dist_step.dependOn(&b.addInstallArtifact(dist_exe, .{}).step);
156+
157+
const run_dist_step = b.step("run-" ++ name, "Run the " ++ name ++ " distribution");
158+
const run_dist_cmd = b.addRunArtifact(dist_exe);
159+
run_dist_step.dependOn(&run_dist_cmd.step);
160+
run_dist_cmd.step.dependOn(b.getInstallStep());
161+
if (b.args) |args| {
162+
run_dist_cmd.addArgs(args);
163+
}
152164
}
153165

154166
// This creates a top level step. Top level steps have a name and can be
@@ -201,10 +213,11 @@ pub fn build(b: *std.Build) void {
201213
const protoc_step = protobuf.RunProtocStep.create(protobuf_dep.builder, target, .{
202214
.destination_directory = b.path("src/proto"),
203215
.source_files = &.{
204-
"proto/policy/opentelemetry/proto/policy/v1/policy.proto",
216+
"proto/opentelemetry/proto/policy/v1/policy.proto",
217+
"proto/opentelemetry/proto/logs/v1/logs.proto",
205218
},
206219
.include_directories = &.{
207-
"proto/policy",
220+
"proto",
208221
},
209222
});
210223
gen_proto.dependOn(&protoc_step.step);

‎config-otlp.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"listen_address": "127.0.0.1",
3+
"listen_port": 8080,
4+
"upstream_url": "https://otlp.us5.datadoghq.com",
5+
"workspace_id": "90A6EFC2-27B8-41BC-9343-43BFB1DF0732",
6+
"log_level": "info",
7+
"max_body_size": 1048576,
8+
"policy_providers": [
9+
{
10+
"id": "file",
11+
"type": "file",
12+
"path": "policies.json"
13+
},
14+
{
15+
"id": "http",
16+
"type": "http",
17+
"url": "http://localhost:9090/v1/policy/sync"
18+
}
19+
]
20+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2020, OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
package opentelemetry.proto.collector.logs.v1;
18+
19+
import "opentelemetry/proto/logs/v1/logs.proto";
20+
21+
option csharp_namespace = "OpenTelemetry.Proto.Collector.Logs.V1";
22+
option java_multiple_files = true;
23+
option java_package = "io.opentelemetry.proto.collector.logs.v1";
24+
option java_outer_classname = "LogsServiceProto";
25+
option go_package = "go.opentelemetry.io/proto/otlp/collector/logs/v1";
26+
27+
// Service that can be used to push logs between one Application instrumented with
28+
// OpenTelemetry and an collector, or between an collector and a central collector (in this
29+
// case logs are sent/received to/from multiple Applications).
30+
service LogsService {
31+
rpc Export(ExportLogsServiceRequest) returns (ExportLogsServiceResponse) {}
32+
}
33+
34+
message ExportLogsServiceRequest {
35+
// An array of ResourceLogs.
36+
// For data coming from a single resource this array will typically contain one
37+
// element. Intermediary nodes (such as OpenTelemetry Collector) that receive
38+
// data from multiple origins typically batch the data before forwarding further and
39+
// in that case this array will contain multiple elements.
40+
repeated opentelemetry.proto.logs.v1.ResourceLogs resource_logs = 1;
41+
}
42+
43+
message ExportLogsServiceResponse {
44+
// The details of a partially successful export request.
45+
//
46+
// If the request is only partially accepted
47+
// (i.e. when the server accepts only parts of the data and rejects the rest)
48+
// the server MUST initialize the `partial_success` field and MUST
49+
// set the `rejected_<signal>` with the number of items it rejected.
50+
//
51+
// Servers MAY also make use of the `partial_success` field to convey
52+
// warnings/suggestions to senders even when the request was fully accepted.
53+
// In such cases, the `rejected_<signal>` MUST have a value of `0` and
54+
// the `error_message` MUST be non-empty.
55+
//
56+
// A `partial_success` message with an empty value (rejected_<signal> = 0 and
57+
// `error_message` = "") is equivalent to it not being set/present. Senders
58+
// SHOULD interpret it the same way as in the full success case.
59+
ExportLogsPartialSuccess partial_success = 1;
60+
}
61+
62+
message ExportLogsPartialSuccess {
63+
// The number of rejected log records.
64+
//
65+
// A `rejected_<signal>` field holding a `0` value indicates that the
66+
// request was fully accepted.
67+
int64 rejected_log_records = 1;
68+
69+
// A developer-facing human-readable message in English. It should be used
70+
// either to explain why the server rejected parts of the data during a partial
71+
// success or to convey warnings/suggestions during a full success. The message
72+
// should offer guidance on how users can address such issues.
73+
//
74+
// error_message is an optional field. An error_message with an empty value
75+
// is equivalent to it not being set.
76+
string error_message = 2;
77+
}

0 commit comments

Comments
 (0)