Skip to content

Commit 6796c5b

Browse files
committed
Move turborepo-otel to the experimental periodic_reader_with_async_runtime feature from opentelemetry_sdk
1 parent d597385 commit 6796c5b

File tree

7 files changed

+235
-5
lines changed

7 files changed

+235
-5
lines changed

crates/turborepo-lib/src/run/summary/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ use turborepo_env::EnvironmentVariableMap;
2727
use turborepo_repository::package_graph::{PackageGraph, PackageName};
2828
use turborepo_scm::SCM;
2929
use turborepo_task_id::TaskId;
30-
use turborepo_ui::{color, cprintln, cwriteln, ColorConfig, BOLD, BOLD_CYAN, GREY};
30+
use turborepo_ui::{BOLD, BOLD_CYAN, ColorConfig, GREY, color, cprintln, cwriteln};
3131

32-
pub(crate) use self::task::TaskSummary; // Re-exported for use in observability/otel.rs
32+
/// Re-exported for use in observability/otel.rs
33+
pub(crate) use self::task::{CacheStatus, TaskSummary};
3334
use self::{
3435
execution::TaskState, task::SinglePackageTaskSummary, task_factory::TaskSummaryFactory,
3536
};

crates/turborepo-otel/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ opentelemetry-otlp = { version = "0.31", features = [
1212
"metrics",
1313
] }
1414
opentelemetry-semantic-conventions = "0.31"
15-
opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "metrics"] }
15+
opentelemetry_sdk = { version = "0.31", features = [
16+
"metrics",
17+
"rt-tokio",
18+
"experimental_async_runtime",
19+
"experimental_metrics_periodicreader_with_async_runtime",
20+
] }
1621
serde = { workspace = true, optional = true }
1722
serde_json = { workspace = true, optional = true }
1823
thiserror = { workspace = true }
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Local OTEL collector example
2+
3+
This example shows how to build the `turbo` binary with the **OpenTelemetry (OTEL)** feature enabled and send metrics to a local collector running via **Docker Compose**. It’s intended as a lightweight, local integration harness for the OTEL exporter.
4+
5+
## 1. Prerequisites
6+
7+
- **Docker & docker compose** installed and running
8+
- **Rust toolchain** (matching this repo’s `rust-toolchain.toml`)
9+
- **pnpm** installed (per `CONTRIBUTING.md`)
10+
11+
All commands below assume the repo root is `turborepo/`.
12+
13+
## 2. Build `turbo` with the OTEL feature
14+
15+
From the repo root:
16+
17+
```bash
18+
pnpm install
19+
cargo build -p turbo --features otel
20+
```
21+
22+
This produces an OTEL-enabled `turbo` binary at: `./target/debug/turbo`
23+
24+
## 3. Start the local collector stack
25+
26+
From this example directory:
27+
28+
```bash
29+
cd crates/turborepo-otel/examples/local-collector
30+
docker compose up -d
31+
```
32+
33+
This starts:
34+
35+
- **`otel-collector`** (OTLP receiver + debug + Prometheus exporter)
36+
- **`prometheus`** (scrapes metrics from the collector)
37+
- **`grafana`** (optional visualization)
38+
39+
Ports:
40+
41+
- **OTLP gRPC**: `4317`
42+
- **OTLP HTTP**: `4318`
43+
- **Collector metrics / Prometheus exporter**: `8888`, `8889`
44+
- **Prometheus UI**: `9090`
45+
- **Grafana UI**: `3000`
46+
47+
You can confirm the collector is ready:
48+
49+
```bash
50+
docker compose logs otel-collector
51+
```
52+
53+
You should see a line similar to:
54+
55+
```text
56+
Everything is ready. Begin running and processing data.
57+
```
58+
59+
## 4. Configure `turbo` to send metrics to the local collector
60+
61+
In a new shell, from the repo root, export the OTEL env vars:
62+
63+
```bash
64+
cd /path/to/turborepo
65+
66+
export TURBO_EXPERIMENTAL_OTEL_ENABLED=1
67+
export TURBO_EXPERIMENTAL_OTEL_PROTOCOL=grpc
68+
export TURBO_EXPERIMENTAL_OTEL_ENDPOINT=http://127.0.0.1:4317
69+
export TURBO_EXPERIMENTAL_OTEL_RESOURCE="service.name=turborepo,env=local"
70+
# Optional (defaults shown)
71+
export TURBO_EXPERIMENTAL_OTEL_METRICS_RUN_SUMMARY=1
72+
export TURBO_EXPERIMENTAL_OTEL_METRICS_TASK_DETAILS=0
73+
```
74+
75+
These environment variables bypass `turbo.json` and directly configure the OTEL exporter.
76+
77+
## 5. Run a task and emit metrics
78+
79+
Use the **locally built** OTEL-enabled binary:
80+
81+
```bash
82+
./target/debug/turbo run lint --filter=turbo-ignore
83+
```
84+
85+
You can replace `lint --filter=turbo-ignore` with any real task in this repo; the important part is that the command finishes so a run summary can be exported.
86+
87+
## 6. Verify metrics reached the collector
88+
89+
- **Collector logs (debug exporter)**:
90+
91+
```bash
92+
cd crates/turborepo-otel/examples/local-collector
93+
docker compose logs --tail=100 otel-collector
94+
```
95+
96+
You should see entries like:
97+
98+
```text
99+
Metrics {"otelcol.component.id": "debug", "otelcol.signal": "metrics", "resource metrics": 1, "metrics": 4, "data points": 4}
100+
Resource attributes:
101+
-> env: Str(local)
102+
-> service.name: Str(turborepo)
103+
Metric #0
104+
-> Name: turbo.run.duration_ms
105+
Metric #1
106+
-> Name: turbo.run.tasks.attempted
107+
Metric #2
108+
-> Name: turbo.run.tasks.failed
109+
Metric #3
110+
-> Name: turbo.run.tasks.cached
111+
```
112+
113+
- **Prometheus UI** (optional):
114+
115+
- Open `http://localhost:9090`
116+
- Query for metrics such as:
117+
- `turbo.run.duration_ms`
118+
- `turbo.run.tasks.attempted`
119+
- `turbo.run.tasks.failed`
120+
- `turbo.run.tasks.cached`
121+
122+
- **Grafana UI** (optional):
123+
124+
- Open `http://localhost:3000` (default credentials are usually `admin` / `admin`)
125+
- Add a Prometheus data source pointing at `http://prometheus:9090`
126+
- Build dashboards using the `turbo.*` metrics
127+
128+
## 7. Cleanup
129+
130+
To stop the local collector stack:
131+
132+
```bash
133+
cd crates/turborepo-otel/examples/local-collector
134+
docker compose down
135+
```
136+
137+
The OTEL-enabled `turbo` binary remains available at `./target/debug/turbo`. You can continue using it with the same environment variables to send metrics to this collector or to another OTLP-compatible backend.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
services:
2+
otel-collector:
3+
image: otel/opentelemetry-collector-contrib:0.139.0
4+
volumes:
5+
- ./otel-collector.yml:/etc/otelcol-contrib/config.yaml
6+
ports:
7+
- 1888:1888 # pprof extension
8+
- 8888:8888 # Prometheus metrics exposed by the Collector
9+
- 8889:8889 # Prometheus exporter metrics
10+
- 13133:13133 # health_check extension
11+
- 4317:4317 # OTLP gRPC receiver
12+
- 4318:4318 # OTLP http receiver
13+
- 55679:55679 # zpages extension
14+
15+
prometheus:
16+
image: prom/prometheus:latest
17+
volumes:
18+
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
19+
ports:
20+
- "9090:9090"
21+
depends_on: [otel-collector]
22+
23+
grafana:
24+
image: grafana/grafana:latest
25+
ports:
26+
- "3000:3000"
27+
depends_on: [prometheus]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
receivers:
2+
otlp:
3+
protocols:
4+
grpc:
5+
endpoint: 0.0.0.0:4317
6+
http:
7+
endpoint: 0.0.0.0:4318
8+
9+
exporters:
10+
debug:
11+
verbosity: detailed
12+
prometheus:
13+
endpoint: "0.0.0.0:8889"
14+
resource_to_telemetry_conversion:
15+
enabled: true
16+
17+
service:
18+
pipelines:
19+
metrics:
20+
receivers: [otlp]
21+
exporters: [debug, prometheus]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
global:
2+
scrape_interval: 5s
3+
4+
scrape_configs:
5+
- job_name: "otel-collector"
6+
static_configs:
7+
- targets: ["otel-collector:8889"]

crates/turborepo-otel/src/lib.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use opentelemetry::{
1111
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, WithTonicConfig};
1212
use opentelemetry_sdk::{
1313
Resource,
14-
metrics::{PeriodicReader, SdkMeterProvider, Temporality},
14+
metrics::{SdkMeterProvider, Temporality, periodic_reader_with_async_runtime},
15+
runtime::Tokio,
1516
};
1617
use opentelemetry_semantic_conventions::resource::SERVICE_NAME;
1718
use thiserror::Error;
@@ -138,6 +139,15 @@ impl Handle {
138139
let meter = provider.meter("turborepo");
139140
let instruments = Arc::new(create_instruments(&meter));
140141

142+
tracing::debug!(
143+
target: "turborepo_otel",
144+
"initialized otel exporter: endpoint={} protocol={:?} run_summary={} task_details={}",
145+
config.endpoint,
146+
config.protocol,
147+
config.metrics.run_summary,
148+
config.metrics.task_details
149+
);
150+
141151
Ok(Self {
142152
inner: Arc::new(HandleInner {
143153
provider,
@@ -148,6 +158,14 @@ impl Handle {
148158
}
149159

150160
pub fn record_run(&self, payload: &RunMetricsPayload) {
161+
tracing::debug!(
162+
target: "turborepo_otel",
163+
"record_run payload: run_id={} attempted={} failed={} cached={}",
164+
payload.run_id,
165+
payload.attempted_tasks,
166+
payload.failed_tasks,
167+
payload.cached_tasks
168+
);
151169
if self.inner.metrics.run_summary {
152170
self.inner.instruments.record_run_summary(payload);
153171
}
@@ -157,6 +175,7 @@ impl Handle {
157175
}
158176

159177
pub fn shutdown(self) {
178+
tracing::debug!(target = "turborepo_otel", "shutting down otel exporter");
160179
match Arc::try_unwrap(self.inner) {
161180
Ok(inner) => {
162181
if let Err(err) = inner.provider.shutdown() {
@@ -174,6 +193,13 @@ impl Handle {
174193

175194
impl Instruments {
176195
fn record_run_summary(&self, payload: &RunMetricsPayload) {
196+
tracing::debug!(
197+
target: "turborepo_otel",
198+
"record_run_summary run_id={} duration_ms={} attempted={}",
199+
payload.run_id,
200+
payload.duration_ms,
201+
payload.attempted_tasks
202+
);
177203
let attrs = build_run_attributes(payload);
178204
self.run_duration.record(payload.duration_ms, &attrs);
179205
self.run_attempted.add(payload.attempted_tasks, &attrs);
@@ -182,6 +208,12 @@ impl Instruments {
182208
}
183209

184210
fn record_task_details(&self, payload: &RunMetricsPayload) {
211+
tracing::debug!(
212+
target: "turborepo_otel",
213+
"record_task_details run_id={} task_count={}",
214+
payload.run_id,
215+
payload.tasks.len()
216+
);
185217
let base_attrs = build_run_attributes(payload);
186218
for task in payload.tasks.iter() {
187219
let mut attrs = base_attrs.clone();
@@ -256,7 +288,7 @@ fn build_provider(config: &Config) -> Result<SdkMeterProvider, Error> {
256288
}
257289
};
258290

259-
let reader = PeriodicReader::builder(exporter)
291+
let reader = periodic_reader_with_async_runtime::PeriodicReader::builder(exporter, Tokio)
260292
.with_interval(Duration::from_secs(15))
261293
.build();
262294

0 commit comments

Comments
 (0)