Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-util/src/telemetry.rs
Line
Count
Source
1
// Copyright 2025 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (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
//    See LICENSE file for details
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
use core::default::Default;
16
use std::collections::HashMap;
17
use std::env;
18
use std::sync::{Arc, OnceLock};
19
20
use base64::Engine;
21
use base64::prelude::BASE64_STANDARD_NO_PAD;
22
use ginepro::LoadBalancedChannel;
23
use hyper::http::Response;
24
use nativelink_error::{Code, Error, ResultExt, make_err};
25
use nativelink_proto::build::bazel::remote::execution::v2::RequestMetadata;
26
use opentelemetry::propagation::{Injector, TextMapCompositePropagator, TextMapPropagator};
27
use opentelemetry::trace::{TraceContextExt, Tracer, TracerProvider};
28
use opentelemetry::{KeyValue, global};
29
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
30
use opentelemetry_http::HeaderExtractor;
31
use opentelemetry_otlp::{
32
    LogExporter, MetricExporter, Protocol, SpanExporter, WithExportConfig, WithTonicConfig,
33
};
34
use opentelemetry_sdk::Resource;
35
use opentelemetry_sdk::logs::SdkLoggerProvider;
36
use opentelemetry_sdk::metrics::SdkMeterProvider;
37
use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};
38
use opentelemetry_sdk::trace::SdkTracerProvider;
39
use opentelemetry_semantic_conventions::attribute::ENDUSER_ID;
40
use prost::Message;
41
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
42
use tracing::debug;
43
use tracing::metadata::LevelFilter;
44
use tracing_opentelemetry::{MetricsLayer, layer};
45
use tracing_subscriber::filter::Directive;
46
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
47
use tracing_subscriber::util::SubscriberInitExt;
48
use tracing_subscriber::{EnvFilter, Layer, Registry, fmt, registry};
49
use uuid::Uuid;
50
51
use crate::metrics::record_rpc_served;
52
use crate::origin_event::{BAZEL_METADATA_KEY, request_metadata_to_baggage};
53
54
/// The OTLP "service.name" field for all nativelink services.
55
const NATIVELINK_SERVICE_NAME: &str = "nativelink";
56
57
// An `EnvFilter` to filter out non-nativelink information.
58
//
59
// See: https://github.com/open-telemetry/opentelemetry-rust/issues/2877
60
//
61
// Note that `EnvFilter` doesn't implement `clone`, so create a new one for
62
// each telemetry kind.
63
0
fn otlp_filter() -> EnvFilter {
64
0
    fn expect_parse(directive: &str) -> Directive {
65
0
        directive
66
0
            .parse()
67
0
            .unwrap_or_else(|_| panic!("Static directive '{directive}' failed to parse"))
68
0
    }
69
70
0
    EnvFilter::builder()
71
0
        .with_default_directive(LevelFilter::INFO.into())
72
0
        .from_env_lossy()
73
0
        .add_directive(expect_parse("hyper=off"))
74
0
        .add_directive(expect_parse("tonic=off"))
75
0
        .add_directive(expect_parse("h2=off"))
76
0
        .add_directive(expect_parse("reqwest=off"))
77
0
        .add_directive(expect_parse("tower=off"))
78
0
}
79
80
// Create a tracing layer intended for stdout printing.
81
//
82
// The output of this layer is configurable via the `NL_LOG` environment
83
// variable.
84
0
fn tracing_stdout_layer() -> impl Layer<Registry> {
85
0
    let nl_log_fmt = env::var("NL_LOG").unwrap_or_else(|_| "pretty".to_string());
86
87
0
    let stdout_filter = otlp_filter();
88
89
0
    match nl_log_fmt.as_str() {
90
0
        "compact" => fmt::layer()
91
0
            .compact()
92
0
            .with_timer(fmt::time::time())
93
0
            .with_filter(stdout_filter)
94
0
            .boxed(),
95
0
        "json" => fmt::layer()
96
0
            .json()
97
0
            .with_timer(fmt::time::time())
98
0
            .with_filter(stdout_filter)
99
0
            .boxed(),
100
0
        _ => fmt::layer()
101
0
            .pretty()
102
0
            .with_timer(fmt::time::time())
103
0
            .with_filter(stdout_filter)
104
0
            .boxed(),
105
    }
106
0
}
107
108
/// Initialize tracing with OpenTelemetry support.
109
///
110
/// # Errors
111
///
112
/// Returns `Err` if logging was already initialized or if the exporters can't
113
/// be initialized.
114
0
pub async fn init_tracing() -> Result<(), Error> {
115
    static INITIALIZED: OnceLock<()> = OnceLock::new();
116
117
    if INITIALIZED.get().is_some() {
118
        return Err(make_err!(Code::Internal, "Logging already initialized"));
119
    }
120
121
    // We currently use a UUIDv4 for "service.instance.id" as per:
122
    // https://opentelemetry.io/docs/specs/semconv/attributes-registry/service/
123
    // This might change as we get a better understanding of its use cases in the
124
    // context of broader observability infrastructure.
125
    let resource = Resource::builder()
126
        .with_service_name(NATIVELINK_SERVICE_NAME)
127
        .with_attribute(KeyValue::new(
128
            "service.instance.id",
129
            Uuid::new_v4().to_string(),
130
        ))
131
        .build();
132
133
    init_propagator();
134
135
    let channels = load_balanced_channels().await?;
136
137
    // Logs
138
    let log_exporter_builder =
139
        with_transport(LogExporter::builder().with_tonic(), channels.logs, "LOGS")?;
140
    let otlp_log_layer = OpenTelemetryTracingBridge::new(
141
        &SdkLoggerProvider::builder()
142
            .with_resource(resource.clone())
143
            .with_batch_exporter(
144
                log_exporter_builder
145
                    .with_protocol(Protocol::Grpc)
146
                    .build()
147
                    .map_err(|e| make_err!(Code::Internal, "{e}"))
148
                    .err_tip(|| "While creating OpenTelemetry OTLP Log exporter")?,
149
            )
150
            .build(),
151
    )
152
    .with_filter(otlp_filter());
153
154
    // Traces
155
    let span_exporter_builder = with_transport(
156
        SpanExporter::builder().with_tonic(),
157
        channels.traces,
158
        "TRACES",
159
    )?;
160
    let otlp_trace_layer = layer()
161
        .with_tracer(
162
            SdkTracerProvider::builder()
163
                .with_resource(resource.clone())
164
                .with_batch_exporter(
165
                    span_exporter_builder
166
                        .with_protocol(Protocol::Grpc)
167
                        .build()
168
                        .map_err(|e| make_err!(Code::Internal, "{e}"))
169
                        .err_tip(|| "While creating OpenTelemetry OTLP Span exporter")?,
170
                )
171
                .build()
172
                .tracer(NATIVELINK_SERVICE_NAME),
173
        )
174
        .with_filter(otlp_filter());
175
176
    // Metrics
177
    let metric_exporter_builder = with_transport(
178
        MetricExporter::builder().with_tonic(),
179
        channels.metrics,
180
        "METRICS",
181
    )?;
182
    let meter_provider = SdkMeterProvider::builder()
183
        .with_resource(resource)
184
        .with_periodic_exporter(
185
            metric_exporter_builder
186
                .with_protocol(Protocol::Grpc)
187
                .build()
188
                .map_err(|e| make_err!(Code::Internal, "{e}"))
189
                .err_tip(|| "While creating OpenTelemetry OTLP Metric exporter")?,
190
        )
191
        .build();
192
193
    global::set_meter_provider(meter_provider.clone());
194
195
    let otlp_metrics_layer = MetricsLayer::new(meter_provider).with_filter(otlp_filter());
196
197
    registry()
198
        .with(tracing_stdout_layer())
199
        .with(otlp_log_layer)
200
        .with(otlp_trace_layer)
201
        .with(otlp_metrics_layer)
202
        .init();
203
204
    INITIALIZED.set(()).unwrap_or(());
205
206
    Ok(())
207
}
208
209
/// Environment variable pointing OTLP exporters at a load-balanced endpoint.
210
pub const NL_OTEL_ENDPOINT: &str = "NL_OTEL_ENDPOINT";
211
212
/// The OTLP signals, as the OpenTelemetry specification spells them in the
213
/// per-signal variables (`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE`).
214
const OTLP_SIGNALS: [&str; 3] = ["LOGS", "TRACES", "METRICS"];
215
216
/// The load-balanced channel each exporter sends on when
217
/// [`NL_OTEL_ENDPOINT`] is set, and `None` for each when it is not.
218
///
219
/// The common case is one channel shared by all three. A channel carries
220
/// its TLS configuration, and an exporter given a channel ignores its own
221
/// `with_tls_config`, so a signal with a certificate variable of its own
222
/// gets a channel of its own, built with that signal's material.
223
#[derive(Debug, Default)]
224
pub struct SignalChannels {
225
    pub logs: Option<LoadBalancedChannel>,
226
    pub traces: Option<LoadBalancedChannel>,
227
    pub metrics: Option<LoadBalancedChannel>,
228
}
229
230
/// Whether any per-signal certificate variable is set and non-empty, which
231
/// is when the signals need channels of their own.
232
4
fn any_per_signal_tls_variable_set() -> bool {
233
10
    
OTLP_SIGNALS.iter()4
.
any4
(|signal| {
234
10
        [OTLP_CERTIFICATE, OTLP_CLIENT_CERTIFICATE, OTLP_CLIENT_KEY]
235
10
            .iter()
236
26
            .
any10
(|suffix| {
237
26
                env::var(format!("OTEL_EXPORTER_OTLP_{signal}_{suffix}"))
238
26
                    .is_ok_and(|value| !
value3
.
is_empty3
())
239
26
            })
240
10
    })
241
4
}
242
243
/// Builds the channels the exporters send on when [`NL_OTEL_ENDPOINT`] is
244
/// set: one shared channel with the general certificate variables, or, when
245
/// any per-signal certificate variable is set, one per signal with that
246
/// signal's resolution of the variables. Without the endpoint variable
247
/// every field is `None` and each exporter builds its own connection.
248
///
249
/// # Errors
250
///
251
/// As [`load_balanced_channel_for`].
252
2
pub async fn load_balanced_channels() -> Result<SignalChannels, Error> {
253
2
    if env::var(NL_OTEL_ENDPOINT).is_err() {
254
1
        return Ok(SignalChannels::default());
255
1
    }
256
1
    if !any_per_signal_tls_variable_set() {
257
0
        let shared = load_balanced_channel_for(None).await?;
258
0
        return Ok(SignalChannels {
259
0
            logs: shared.clone(),
260
0
            traces: shared.clone(),
261
0
            metrics: shared,
262
0
        });
263
1
    }
264
    Ok(SignalChannels {
265
1
        logs: load_balanced_channel_for(Some("LOGS")).await?,
266
0
        traces: load_balanced_channel_for(Some("TRACES")).await?,
267
0
        metrics: load_balanced_channel_for(Some("METRICS")).await?,
268
    })
269
2
}
270
271
/// Creates a load-balanced gRPC channel for the endpoint configured via
272
/// [`NL_OTEL_ENDPOINT`] with the general certificate variables, or returns
273
/// `None` if the variable isn't set. This is [`load_balanced_channel_for`]
274
/// with no signal, kept for the integration tests that verify exporters
275
/// behave the same with and without the load-balanced channel;
276
/// [`init_tracing`] goes through [`load_balanced_channels`].
277
///
278
/// # Errors
279
///
280
/// As [`load_balanced_channel_for`].
281
2
pub async fn maybe_load_balanced_channel() -> Result<Option<LoadBalancedChannel>, Error>1
{
282
2
    load_balanced_channel_for(None).await
283
2
}
284
285
/// Creates a load-balanced gRPC channel for the endpoint configured via
286
/// [`NL_OTEL_ENDPOINT`], or returns `None` if the variable isn't set. An
287
/// `https://` endpoint gets TLS, with the material the OpenTelemetry
288
/// certificate variables name for `signal` (see [`otlp_tls_config`]) or the
289
/// system trust store without them; a plaintext endpoint gets no TLS
290
/// configuration at all.
291
///
292
/// # Errors
293
///
294
/// Returns `Err` if the endpoint is not a URL with a host and an explicit
295
/// port, if its hostname cannot be resolved, or if the certificate variables
296
/// name files that cannot be read.
297
5
pub async fn load_balanced_channel_for(
298
5
    signal: Option<&str>,
299
5
) -> Result<Option<LoadBalancedChannel>, Error> {
300
5
    let Ok(
endpoint3
) = env::var(NL_OTEL_ENDPOINT) else {
301
2
        return Ok(None);
302
    };
303
3
    let url = Url::parse(endpoint.as_str())
304
3
        .map_err(|e| 
{0
305
0
            make_err!(
306
0
                Code::InvalidArgument,
307
                "Unable to parse endpoint {endpoint}: {e:?}"
308
            )
309
0
        })
310
3
        .err_tip(|| 
format!0
("In {NL_OTEL_ENDPOINT}"))
?0
;
311
312
3
    let host = url
313
3
        .host()
314
3
        .err_tip(|| 
format!0
("Unable to get host from endpoint {endpoint}"))
?0
;
315
3
    let port = url
316
3
        .port()
317
3
        .err_tip(|| 
format!0
("Unable to get port from endpoint {endpoint}"))
?0
;
318
319
3
    let mut builder = LoadBalancedChannel::builder((host.to_string(), port));
320
3
    if url.scheme() == "https" {
321
3
        let 
tls_config1
= otlp_tls_config(signal)
?2
.
unwrap_or_else1
(system_roots_tls_config);
322
1
        builder = builder.with_tls(tls_config);
323
0
    }
324
1
    builder
325
1
        .channel()
326
1
        .await
327
1
        .map_err(|e| make_err!(Code::Internal, "Invalid hostname '{endpoint}': {e}"))
328
1
        .map(Some)
329
5
}
330
331
/// The OpenTelemetry specification's variables for the OTLP exporters' TLS
332
/// material, as the suffix after `OTEL_EXPORTER_OTLP_`: the CA that signs
333
/// the collector's certificate, and the certificate and key the exporter
334
/// presents when the collector asks for one. `opentelemetry-otlp` 0.32 does
335
/// not read these itself, so they are read here.
336
const OTLP_CERTIFICATE: &str = "CERTIFICATE";
337
const OTLP_CLIENT_CERTIFICATE: &str = "CLIENT_CERTIFICATE";
338
const OTLP_CLIENT_KEY: &str = "CLIENT_KEY";
339
340
/// The default OTLP endpoint of the gRPC exporters, as `opentelemetry-otlp`
341
/// resolves it when no endpoint variable is set.
342
const OTLP_GRPC_ENDPOINT_DEFAULT: &str = "http://localhost:4317";
343
344
/// Looks an OTLP variable up the way the specification orders them: the
345
/// per-signal form (`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE`) wins over the
346
/// general one (`OTEL_EXPORTER_OTLP_CERTIFICATE`), and an empty value counts
347
/// as unset. Returns the name that was found with its value, for messages.
348
70
fn otlp_env_var(signal: Option<&str>, suffix: &str) -> Option<(String, String)> {
349
70
    signal
350
70
        .map(|signal| 
format!61
("OTEL_EXPORTER_OTLP_{signal}_{suffix}"))
351
70
        .into_iter()
352
70
        .chain(core::iter::once(format!("OTEL_EXPORTER_OTLP_{suffix}")))
353
124
        .
find_map70
(|name| {
354
124
            env::var(&name)
355
124
                .ok()
356
124
                .filter(|value| !
value23
.
is_empty23
())
357
124
                .map(|value| (
name22
,
value22
))
358
124
        })
359
70
}
360
361
/// Whether the exporter for `signal` will talk to an `https://` endpoint,
362
/// resolved the way `opentelemetry-otlp` resolves it. The scheme is
363
/// compared case-insensitively, as the SDK accepts `HTTPS://` too; a value
364
/// that does not parse as a URL counts as plaintext and is left for the
365
/// exporter to reject. (The `NL_OTEL_ENDPOINT` path parses its value with
366
/// `Url` as well, so it judges the scheme the same way.)
367
10
fn otlp_endpoint_is_https(signal: &str) -> bool {
368
10
    let endpoint = otlp_env_var(Some(signal), "ENDPOINT").map_or_else(
369
4
        || OTLP_GRPC_ENDPOINT_DEFAULT.to_string(),
370
        |(_, value)| value,
371
    );
372
10
    Url::parse(&endpoint).is_ok_and(|url| url.scheme() == "https")
373
10
}
374
375
/// A TLS configuration that trusts the system's certificate store: what an
376
/// `https://` collector gets when no certificate variable names a CA.
377
/// tonic's bare `ClientTlsConfig::new()` trusts nothing at all.
378
7
fn system_roots_tls_config() -> ClientTlsConfig {
379
7
    ClientTlsConfig::new().with_enabled_roots()
380
7
}
381
382
14
fn read_pem((name, path): &(String, String)) -> Result<Vec<u8>, Error> {
383
14
    std::fs::read(path).map_err(|e| 
{6
384
6
        make_err!(
385
6
            Code::InvalidArgument,
386
            "Could not read {path}, which {name} names: {e}"
387
        )
388
6
    })
389
14
}
390
391
/// The TLS configuration the OpenTelemetry certificate variables describe
392
/// for `signal` (`LOGS`, `TRACES` or `METRICS`; `None` reads only the
393
/// general variables), or `None` when none of them is set.
394
///
395
/// `OTEL_EXPORTER_OTLP_CERTIFICATE` names the CA to verify the collector
396
/// with, and then only that CA is trusted; without it the system trust store
397
/// is. `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` and
398
/// `OTEL_EXPORTER_OTLP_CLIENT_KEY` name the identity to present for mTLS
399
/// and have to be set together. Each has a per-signal form that wins over
400
/// the general one. The files are read here, once, at startup.
401
///
402
/// # Errors
403
///
404
/// Returns `Err` if a named file cannot be read, or if only one of the
405
/// client certificate and key is set.
406
20
pub fn otlp_tls_config(signal: Option<&str>) -> Result<Option<ClientTlsConfig>, Error> {
407
20
    let ca = otlp_env_var(signal, OTLP_CERTIFICATE);
408
20
    let client_certificate = otlp_env_var(signal, OTLP_CLIENT_CERTIFICATE);
409
20
    let client_key = otlp_env_var(signal, OTLP_CLIENT_KEY);
410
20
    if ca.is_none() && 
client_certificate10
.
is_none10
() &&
client_key7
.
is_none7
() {
411
6
        return Ok(None);
412
14
    }
413
14
    let 
mut config8
= match &ca {
414
10
        Some(ca) => ClientTlsConfig::new().
ca_certificate4
(
Certificate::from_pem4
(read_pem(ca)
?6
)),
415
4
        None => system_roots_tls_config(),
416
    };
417
8
    match (&client_certificate, &client_key) {
418
2
        (Some(certificate), Some(key)) => {
419
2
            config = config.identity(Identity::from_pem(read_pem(certificate)
?0
, read_pem(key)
?0
));
420
        }
421
4
        (None, None) => {}
422
1
        (Some((name, _)), None) | (None, Some((name, _))) => {
423
2
            return Err(make_err!(
424
2
                Code::InvalidArgument,
425
2
                "{name} is set without its pair: a client certificate and its key have to be \
426
2
                 set together, as OTEL_EXPORTER_OTLP_{OTLP_CLIENT_CERTIFICATE} and \
427
2
                 OTEL_EXPORTER_OTLP_{OTLP_CLIENT_KEY} or their per-signal forms"
428
2
            ));
429
        }
430
    }
431
6
    Ok(Some(config))
432
20
}
433
434
/// The TLS configuration for the exporter of `signal` when it builds its
435
/// own channel: nothing for a plaintext endpoint, whatever the certificate
436
/// variables say, and for an `https://` endpoint the variables' material or
437
/// the system trust store without them. A plaintext signal must get no
438
/// configuration at all: the exporter would still hand it to tonic, which
439
/// loads the trust roots as it builds the connector and fails on a host
440
/// without a certificate bundle, so a deployment that sends one signal
441
/// over TLS and another in plaintext would die building the plaintext one.
442
10
fn exporter_tls_config(signal: &str) -> Result<Option<ClientTlsConfig>, Error> {
443
10
    if !otlp_endpoint_is_https(signal) {
444
5
        return Ok(None);
445
5
    }
446
    Ok(Some(
447
5
        otlp_tls_config(Some(signal))
?1
.
unwrap_or_else4
(system_roots_tls_config),
448
    ))
449
10
}
450
451
/// Gives the exporter builder for `signal` its transport: the channel
452
/// [`NL_OTEL_ENDPOINT`] supplied, which carries its own TLS configuration,
453
/// or else the TLS configuration the signal's own endpoint calls for. With
454
/// a channel the SDK's endpoint is ignored, so it is not examined either:
455
/// an `OTEL_EXPORTER_OTLP_ENDPOINT` left pointing at an `https://`
456
/// collector, with a certificate file that is no longer there, must not
457
/// stop a process that is not going to connect to it.
458
1
fn with_transport<B: WithTonicConfig>(
459
1
    builder: B,
460
1
    channel: Option<LoadBalancedChannel>,
461
1
    signal: &str,
462
1
) -> Result<B, Error> {
463
1
    if let Some(
channel0
) = channel {
464
0
        return Ok(builder.with_channel(channel.into()));
465
1
    }
466
1
    Ok(match exporter_tls_config(signal)? {
467
0
        Some(tls_config) => builder.with_tls_config(tls_config),
468
0
        None => builder,
469
    })
470
1
}
471
/// This is the header that bazel sends when using the `--remote_header` flag.
472
/// TODO(palfrey): Bazel supports other headers, and we should optimize their usage.
473
const BAZEL_REQUESTMETADATA_HEADER: &str = "build.bazel.remote.execution.v2.requestmetadata-bin";
474
475
use opentelemetry::baggage::BaggageExt;
476
use opentelemetry::context::FutureExt;
477
use url::Url;
478
479
/// ASCII headers from an inbound client request, stored in the task context
480
/// so that outgoing upstream calls can forward them (e.g. JWT auth tokens).
481
#[derive(Clone, Debug, Default)]
482
pub struct ClientHeaders(pub Arc<HashMap<String, String>>);
483
484
#[derive(Debug, Clone)]
485
pub struct OtlpMiddleware<S> {
486
    inner: S,
487
    identity_required: bool,
488
}
489
490
impl<S> OtlpMiddleware<S> {
491
5
    const fn new(inner: S, identity_required: bool) -> Self {
492
5
        Self {
493
5
            inner,
494
5
            identity_required,
495
5
        }
496
5
    }
497
}
498
499
impl<S, ReqBody, ResBody> tower::Service<hyper::http::Request<ReqBody>> for OtlpMiddleware<S>
500
where
501
    S: tower::Service<hyper::http::Request<ReqBody>, Response = Response<ResBody>>
502
        + Clone
503
        + Send
504
        + 'static,
505
    S::Future: Send + 'static,
506
    ReqBody: core::fmt::Debug + Send + 'static,
507
    ResBody: From<String> + Send + 'static + Default,
508
{
509
    type Response = S::Response;
510
    type Error = S::Error;
511
    type Future = futures::future::BoxFuture<'static, Result<Self::Response, Self::Error>>;
512
513
6
    fn poll_ready(
514
6
        &mut self,
515
6
        cx: &mut core::task::Context<'_>,
516
6
    ) -> core::task::Poll<Result<(), Self::Error>> {
517
6
        self.inner.poll_ready(cx)
518
6
    }
519
520
6
    fn call(&mut self, req: hyper::http::Request<ReqBody>) -> Self::Future {
521
        // We must take the current `inner` and not the clone.
522
        // See: <https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services>
523
6
        let clone = self.inner.clone();
524
6
        let mut inner = core::mem::replace(&mut self.inner, clone);
525
526
        // Capture all ASCII-valued request headers before req is consumed, so
527
        // they can be forwarded to upstream services (e.g. JWT auth tokens).
528
6
        let client_headers = ClientHeaders(Arc::new(
529
6
            req.headers()
530
6
                .iter()
531
17
                .
filter_map6
(|(name, value)| {
532
17
                    value
533
17
                        .to_str()
534
17
                        .ok()
535
17
                        .map(|v| (name.as_str().to_lowercase(), v.to_string()))
536
17
                })
537
6
                .collect(),
538
        ));
539
540
6
        let parent_cx = global::get_text_map_propagator(|propagator| {
541
6
            propagator.extract(&HeaderExtractor(req.headers()))
542
6
        });
543
544
6
        let identity = parent_cx
545
6
            .baggage()
546
6
            .get(ENDUSER_ID)
547
6
            .map(|value| 
value.as_str()4
.
to_string4
())
548
6
            .unwrap_or_default();
549
550
6
        if identity.is_empty() {
551
2
            if self.identity_required {
552
0
                return Box::pin(async move {
553
0
                    Ok(tonic::Status::failed_precondition(
554
0
                        r"
555
0
556
0
NativeLink instance configured to require this OpenTelemetry Baggage header:
557
0
558
0
    `Baggage: enduser.id=YOUR_IDENTITY`
559
0
560
0
",
561
0
                    )
562
0
                    .into_http())
563
0
                });
564
2
            }
565
        } else {
566
4
            debug!("Baggage enduser.id: {identity}");
567
        }
568
569
6
        let tracer = global::tracer("origin_middleware");
570
6
        let span = tracer
571
6
            .span_builder("origin_request")
572
6
            .with_kind(opentelemetry::trace::SpanKind::Server)
573
6
            .start_with_context(&tracer, &parent_cx);
574
575
6
        let mut cx = parent_cx.with_span(span);
576
577
6
        if let Some(
bazel_header0
) = req.headers().get(BAZEL_REQUESTMETADATA_HEADER)
578
0
            && let Ok(decoded) = BASE64_STANDARD_NO_PAD.decode(bazel_header.as_bytes())
579
0
            && let Ok(metadata) = RequestMetadata::decode(decoded.as_slice())
580
        {
581
0
            let metadata_str = format!("{metadata:?}");
582
0
            debug!("Baggage Bazel request metadata: {metadata_str}");
583
0
            cx = cx.with_baggage(vec![
584
0
                KeyValue::new(BAZEL_METADATA_KEY, request_metadata_to_baggage(&metadata)),
585
0
                KeyValue::new(ENDUSER_ID, identity),
586
            ]);
587
6
        }
588
589
6
        let cx = cx.with_value(client_headers);
590
        // Rate, errors and latency for every gRPC service, taken here because
591
        // this layer already wraps all of them.
592
6
        let route = req.uri().path().to_string();
593
6
        let started = std::time::Instant::now();
594
6
        Box::pin(async move {
595
6
            let result = inner.call(req).with_context(cx).await;
596
6
            if let Ok(response) = &result {
597
6
                record_rpc_served(
598
6
                    &route,
599
6
                    grpc_status_of(response.headers()),
600
6
                    started.elapsed().as_secs_f64(),
601
6
                );
602
6
            
}0
603
6
            result
604
6
        })
605
6
    }
606
}
607
608
/// Reads the gRPC status from a response.
609
///
610
/// Tonic puts `grpc-status` in the headers when it fails before streaming and
611
/// in the trailers otherwise. Trailers are not available here without
612
/// consuming the body, so a call that got far enough to stream is reported as
613
/// OK. Transport-level failures never reach this point at all.
614
6
fn grpc_status_of(headers: &hyper::http::HeaderMap) -> i32 {
615
6
    headers
616
6
        .get("grpc-status")
617
6
        .and_then(|value| 
value0
.
to_str0
().
ok0
())
618
6
        .and_then(|value| 
value0
.
parse0
::<i32>().
ok0
())
619
6
        .unwrap_or(0)
620
6
}
621
622
#[derive(Debug, Clone, Copy)]
623
pub struct OtlpLayer {
624
    identity_required: bool,
625
}
626
627
impl OtlpLayer {
628
0
    pub const fn new(identity_required: bool) -> Self {
629
0
        Self { identity_required }
630
0
    }
631
}
632
633
impl<S> tower::Layer<S> for OtlpLayer {
634
    type Service = OtlpMiddleware<S>;
635
636
5
    fn layer(&self, service: S) -> Self::Service {
637
5
        OtlpMiddleware::new(service, self.identity_required)
638
5
    }
639
}
640
641
/// Installs the propagator the servers extract with and the forwarders inject
642
/// with: W3C baggage and trace context.
643
1
pub fn init_propagator() {
644
1
    global::set_text_map_propagator(TextMapCompositePropagator::new(vec![
645
1
        Box::new(BaggagePropagator::new()),
646
1
        Box::new(TraceContextPropagator::new()),
647
    ]));
648
1
}
649
650
/// Copies the nominated headers of the inbound client request, as the
651
/// [`OtlpMiddleware`] stored them in the context, onto an outgoing request.
652
/// This is how a client's credentials reach the upstream behind a forwarding
653
/// store or scheduler; names match case-insensitively.
654
57
pub fn forward_client_headers<T>(request: &mut tonic::Request<T>, names: &[String]) {
655
57
    if names.is_empty() {
656
52
        return;
657
5
    }
658
5
    let Some(client_headers) = opentelemetry::Context::current()
659
5
        .get::<ClientHeaders>()
660
5
        .cloned()
661
    else {
662
0
        return;
663
    };
664
5
    for name in names {
665
5
        let Some(value) = client_headers.0.get(&name.to_lowercase()) else {
666
0
            continue;
667
        };
668
        // Tonic rejects a `-bin` name as an ASCII key, so a binary header such
669
        // as Bazel's request metadata needs the binary API.
670
5
        if name.to_lowercase().ends_with("-bin") {
671
1
            if let (Ok(k), Ok(bytes)) = (
672
1
                tonic::metadata::MetadataKey::<tonic::metadata::Binary>::from_bytes(
673
1
                    name.as_bytes(),
674
                ),
675
1
                BASE64_STANDARD_NO_PAD.decode(value.trim_end_matches('=')),
676
1
            ) {
677
1
                request
678
1
                    .metadata_mut()
679
1
                    .insert_bin(k, tonic::metadata::MetadataValue::from_bytes(&bytes));
680
1
            
}0
681
4
        } else if let (Ok(k), Ok(v)) = (
682
4
            tonic::metadata::MetadataKey::from_bytes(name.as_bytes()),
683
4
            tonic::metadata::MetadataValue::try_from(value.as_str()),
684
4
        ) {
685
4
            request.metadata_mut().insert(k, v);
686
4
        
}0
687
    }
688
57
}
689
690
/// Writes a context's trace and baggage into an outgoing gRPC request, so a
691
/// service that forwards a call (a grpc store, a grpc scheduler) hands the
692
/// upstream the identity, the client's build metadata and the trace it was
693
/// given instead of a bare request.
694
57
pub fn inject_context_with<T>(
695
57
    propagator: &dyn TextMapPropagator,
696
57
    context: &opentelemetry::Context,
697
57
    request: &mut tonic::Request<T>,
698
57
) {
699
    struct MetadataInjector<'a>(&'a mut tonic::metadata::MetadataMap);
700
701
    impl Injector for MetadataInjector<'_> {
702
4
        fn set(&mut self, key: &str, value: String) {
703
4
            if let (Ok(k), Ok(v)) = (
704
4
                tonic::metadata::MetadataKey::from_bytes(key.as_bytes()),
705
4
                tonic::metadata::MetadataValue::try_from(&value),
706
4
            ) {
707
4
                self.0.insert(k, v);
708
4
            
}0
709
4
        }
710
    }
711
712
57
    propagator.inject_context(context, &mut MetadataInjector(request.metadata_mut()));
713
57
}
714
715
/// [`inject_context_with`] for the current context and the global propagator.
716
56
pub fn inject_context<T>(request: &mut tonic::Request<T>) {
717
56
    global::get_text_map_propagator(|propagator| {
718
56
        inject_context_with(propagator, &opentelemetry::Context::current(), request);
719
56
    });
720
56
}
721
722
#[cfg(test)]
723
mod tests {
724
    use nativelink_macro::nativelink_test;
725
    use serial_test::serial;
726
727
    use super::*;
728
729
    // ginepro's default resolver (hickory-dns) reads /etc/resolv.conf, which
730
    // doesn't exist in sandboxed environments (e.g. Nix builds).
731
    #[cfg(unix)]
732
4
    fn dns_configured() -> bool {
733
4
        std::path::Path::new("/etc/resolv.conf").exists()
734
4
    }
735
    #[cfg(not(unix))]
736
    const fn dns_configured() -> bool {
737
        true
738
    }
739
740
    // Env vars are process-global, so concurrent writes would be a data race
741
    // on Unix.  `#[serial(env)]` serializes all env-mutating tests.
742
    #[serial(env)]
743
    #[nativelink_test("crate")]
744
    async fn channel_absent_when_env_not_set() {
745
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
746
        unsafe { env::remove_var(NL_OTEL_ENDPOINT) };
747
        assert!(
748
            maybe_load_balanced_channel()
749
                .await
750
                .expect("an unset endpoint is not an error")
751
                .is_none(),
752
            "Expected None when {NL_OTEL_ENDPOINT} is unset"
753
        );
754
    }
755
756
    #[serial(env)]
757
    #[nativelink_test("crate")]
758
    async fn channel_present_when_valid_endpoint_set() {
759
        if !dns_configured() {
760
            eprintln!(
761
                "Skipping channel_present_when_valid_endpoint_set: no DNS configuration \
762
                 available (e.g. sandboxed Nix build)"
763
            );
764
            return;
765
        }
766
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
767
        unsafe { env::set_var(NL_OTEL_ENDPOINT, "http://localhost:4317") };
768
        let result = maybe_load_balanced_channel().await;
769
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
770
        unsafe { env::remove_var(NL_OTEL_ENDPOINT) };
771
        assert!(
772
            result.expect("a valid endpoint builds").is_some(),
773
            "Expected Some(channel) when {NL_OTEL_ENDPOINT} points to a valid URL"
774
        );
775
    }
776
777
    /// Clears every OTLP variable a TLS test may set, before and after it.
778
27
    fn clear_otlp_tls_env() {
779
108
        for signal in 
["", 27
"LOGS_"27
, "TRACES_", "METRICS_"] {
780
432
            for suffix in [
781
108
                "ENDPOINT",
782
108
                "CERTIFICATE",
783
108
                "CLIENT_CERTIFICATE",
784
108
                "CLIENT_KEY",
785
432
            ] {
786
432
                // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
787
432
                unsafe { env::remove_var(format!("OTEL_EXPORTER_OTLP_{signal}{suffix}")) };
788
432
            }
789
        }
790
27
    }
791
792
    /// Writes dummy PEM bytes to a file in `dir`; tonic only stores them
793
    /// until a connection is made, so the content need not parse.
794
8
    fn pem_file(dir: &tempfile::TempDir, name: &str) -> String {
795
8
        let path = dir.path().join(name);
796
8
        std::fs::write(&path, format!("-----BEGIN {name}-----\n")).unwrap();
797
8
        path.to_str().unwrap().to_string()
798
8
    }
799
800
    // The exporter used to refuse an https endpoint outright ("uses HTTPS
801
    // but no TLS feature is enabled"); this fails without `tls-ring` on
802
    // `opentelemetry-otlp`.
803
    #[serial(env)]
804
    #[nativelink_test("crate")]
805
    async fn https_endpoint_builds_an_exporter() {
806
        clear_otlp_tls_env();
807
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
808
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "https://localhost:4317") };
809
        let mut builder = SpanExporter::builder().with_tonic();
810
        if let Some(tls_config) = exporter_tls_config("TRACES").unwrap() {
811
            builder = builder.with_tls_config(tls_config);
812
        }
813
        let result = builder.with_protocol(Protocol::Grpc).build();
814
        clear_otlp_tls_env();
815
        assert!(
816
            result.is_ok(),
817
            "An https endpoint must build an exporter: {:?}",
818
            result.err()
819
        );
820
    }
821
822
    #[serial(env)]
823
    #[nativelink_test("crate")]
824
    async fn tls_config_absent_without_variables() {
825
        clear_otlp_tls_env();
826
        assert!(otlp_tls_config(Some("TRACES")).unwrap().is_none());
827
        assert!(otlp_tls_config(None).unwrap().is_none());
828
        // A plaintext endpoint gets no TLS configuration at all, so a host
829
        // without a certificate store still exports.
830
        assert!(exporter_tls_config("TRACES").unwrap().is_none());
831
        // An empty value is unset, as the specification says.
832
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
833
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CERTIFICATE", "") };
834
        let result = otlp_tls_config(Some("TRACES"));
835
        clear_otlp_tls_env();
836
        assert!(result.unwrap().is_none());
837
    }
838
839
    #[serial(env)]
840
    #[nativelink_test("crate")]
841
    async fn tls_config_present_with_ca_certificate() {
842
        clear_otlp_tls_env();
843
        let dir = tempfile::tempdir().unwrap();
844
        let ca = pem_file(&dir, "ca");
845
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
846
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CERTIFICATE", &ca) };
847
        let general = otlp_tls_config(None);
848
        let per_signal = otlp_tls_config(Some("METRICS"));
849
        // The endpoint is the plaintext default, so the exporter gets no
850
        // TLS configuration however the variables are set.
851
        let exporter = exporter_tls_config("METRICS");
852
        clear_otlp_tls_env();
853
        assert!(general.unwrap().is_some());
854
        assert!(per_signal.unwrap().is_some());
855
        assert!(exporter.unwrap().is_none());
856
    }
857
858
    // A deployment that sends metrics over TLS with an identity and logs in
859
    // plaintext: the plaintext signal must get no TLS configuration, or the
860
    // exporter loads the trust roots for it and a host without a
861
    // certificate bundle dies building the log exporter.
862
    #[serial(env)]
863
    #[nativelink_test("crate")]
864
    async fn plaintext_signal_gets_no_tls_config_whatever_the_variables() {
865
        clear_otlp_tls_env();
866
        let dir = tempfile::tempdir().unwrap();
867
        let certificate = pem_file(&dir, "client");
868
        let key = pem_file(&dir, "key");
869
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
870
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", &certificate) };
871
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
872
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CLIENT_KEY", &key) };
873
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
874
        unsafe {
875
            env::set_var(
876
                "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
877
                "https://collector:4317",
878
            );
879
        };
880
        let logs = exporter_tls_config("LOGS");
881
        let metrics = exporter_tls_config("METRICS");
882
        clear_otlp_tls_env();
883
        assert!(
884
            logs.unwrap().is_none(),
885
            "the plaintext default endpoint gets no TLS configuration"
886
        );
887
        assert!(
888
            metrics.unwrap().is_some(),
889
            "the https endpoint gets the variables' configuration"
890
        );
891
    }
892
893
    // The SDK accepts an uppercase scheme; an `HTTPS://` endpoint has to
894
    // get its trust roots too, or the process starts with an empty store.
895
    #[serial(env)]
896
    #[nativelink_test("crate")]
897
    async fn uppercase_https_scheme_gets_the_default_roots() {
898
        clear_otlp_tls_env();
899
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
900
        unsafe {
901
            env::set_var(
902
                "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
903
                "HTTPS://collector:4317",
904
            );
905
        };
906
        let traces = exporter_tls_config("TRACES");
907
        // A value that is not a URL is not https; the exporter rejects it.
908
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
909
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "collector:4317:https") };
910
        let unparsable = exporter_tls_config("TRACES");
911
        clear_otlp_tls_env();
912
        assert!(traces.unwrap().is_some(), "HTTPS:// is an https endpoint");
913
        assert!(unparsable.unwrap().is_none());
914
    }
915
916
    #[serial(env)]
917
    #[nativelink_test("crate")]
918
    async fn tls_config_with_client_identity() {
919
        clear_otlp_tls_env();
920
        let dir = tempfile::tempdir().unwrap();
921
        let certificate = pem_file(&dir, "client");
922
        let key = pem_file(&dir, "key");
923
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
924
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", &certificate) };
925
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
926
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CLIENT_KEY", &key) };
927
        let result = otlp_tls_config(Some("LOGS"));
928
        clear_otlp_tls_env();
929
        assert!(result.unwrap().is_some());
930
    }
931
932
    #[serial(env)]
933
    #[nativelink_test("crate")]
934
    async fn per_signal_variable_wins_over_the_general_one() {
935
        clear_otlp_tls_env();
936
        let dir = tempfile::tempdir().unwrap();
937
        let ca = pem_file(&dir, "ca");
938
        let missing = dir.path().join("missing").to_str().unwrap().to_string();
939
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
940
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CERTIFICATE", &missing) };
941
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
942
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", &ca) };
943
        let traces = otlp_tls_config(Some("TRACES"));
944
        let metrics = otlp_tls_config(Some("METRICS"));
945
        let general = otlp_tls_config(None);
946
        clear_otlp_tls_env();
947
        assert!(traces.unwrap().is_some(), "TRACES names a readable file");
948
        let metrics_err = metrics.expect_err("METRICS falls back to the general, missing file");
949
        assert_eq!(metrics_err.code, Code::InvalidArgument);
950
        assert!(
951
            metrics_err
952
                .to_string()
953
                .contains("OTEL_EXPORTER_OTLP_CERTIFICATE"),
954
            "{metrics_err}"
955
        );
956
        assert_eq!(
957
            general.expect_err("the general file is missing").code,
958
            Code::InvalidArgument
959
        );
960
    }
961
962
    #[serial(env)]
963
    #[nativelink_test("crate")]
964
    async fn client_certificate_without_key_is_rejected() {
965
        clear_otlp_tls_env();
966
        let dir = tempfile::tempdir().unwrap();
967
        let certificate = pem_file(&dir, "client");
968
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
969
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", &certificate) };
970
        let without_key = otlp_tls_config(Some("LOGS"));
971
        clear_otlp_tls_env();
972
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
973
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY", &certificate) };
974
        let without_certificate = otlp_tls_config(Some("LOGS"));
975
        clear_otlp_tls_env();
976
        for result in [without_key, without_certificate] {
977
            let err = result.expect_err("half an identity is an error");
978
            assert_eq!(err.code, Code::InvalidArgument);
979
            assert!(
980
                err.to_string().contains("OTEL_EXPORTER_OTLP_CLIENT_KEY"),
981
                "{err}"
982
            );
983
        }
984
    }
985
986
    #[serial(env)]
987
    #[nativelink_test("crate")]
988
    async fn missing_certificate_file_is_rejected() {
989
        clear_otlp_tls_env();
990
        let dir = tempfile::tempdir().unwrap();
991
        let missing = dir.path().join("missing.pem").to_str().unwrap().to_string();
992
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
993
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CERTIFICATE", &missing) };
994
        let result = otlp_tls_config(Some("METRICS"));
995
        clear_otlp_tls_env();
996
        let err = result.expect_err("a missing file is an error");
997
        assert_eq!(err.code, Code::InvalidArgument);
998
        assert!(err.to_string().contains("missing.pem"), "{err}");
999
        assert!(
1000
            err.to_string().contains("OTEL_EXPORTER_OTLP_CERTIFICATE"),
1001
            "{err}"
1002
        );
1003
    }
1004
1005
    // A per-signal certificate variable has to reach the channel the
1006
    // exporter is handed: an exporter given a channel ignores its own TLS
1007
    // configuration, so a shared channel built from the general variables
1008
    // alone failed the handshake with "unknown CA" for a signal whose CA
1009
    // is set per signal.
1010
    #[serial(env)]
1011
    #[nativelink_test("crate")]
1012
    async fn per_signal_certificate_reaches_the_load_balanced_channel() {
1013
        clear_otlp_tls_env();
1014
        let dir = tempfile::tempdir().unwrap();
1015
        let ca = pem_file(&dir, "ca");
1016
        let missing = dir.path().join("missing").to_str().unwrap().to_string();
1017
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1018
        unsafe { env::set_var(NL_OTEL_ENDPOINT, "https://localhost:4317") };
1019
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1020
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CERTIFICATE", &missing) };
1021
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1022
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", &ca) };
1023
        assert!(any_per_signal_tls_variable_set());
1024
        let traces = load_balanced_channel_for(Some("TRACES")).await;
1025
        let metrics = load_balanced_channel_for(Some("METRICS")).await;
1026
        let all = load_balanced_channels().await;
1027
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1028
        unsafe { env::remove_var(NL_OTEL_ENDPOINT) };
1029
        clear_otlp_tls_env();
1030
1031
        // METRICS falls back to the general variable, whose file is
1032
        // missing; that error precedes any name resolution, so it holds
1033
        // with or without DNS, and so does the whole-set build that
1034
        // includes METRICS.
1035
        for result in [metrics, all.map(|channels| channels.metrics)] {
1036
            let err = result.expect_err("the general CA file is missing");
1037
            assert_eq!(err.code, Code::InvalidArgument);
1038
            assert!(
1039
                err.to_string().contains("OTEL_EXPORTER_OTLP_CERTIFICATE"),
1040
                "{err}"
1041
            );
1042
        }
1043
        // TRACES reads its own file; building the channel then resolves
1044
        // the host, which needs DNS.
1045
        if dns_configured() {
1046
            assert!(
1047
                traces.expect("TRACES names a readable CA").is_some(),
1048
                "Expected a channel for TRACES when {NL_OTEL_ENDPOINT} is set"
1049
            );
1050
        } else {
1051
            eprintln!(
1052
                "Skipping the TRACES channel assertion: no DNS configuration \
1053
                 available (e.g. sandboxed Nix build)"
1054
            );
1055
        }
1056
    }
1057
1058
    // Without a per-signal variable the three signals share one channel,
1059
    // and without the endpoint variable they get none.
1060
    #[serial(env)]
1061
    #[nativelink_test("crate")]
1062
    async fn signals_share_one_channel_without_per_signal_variables() {
1063
        clear_otlp_tls_env();
1064
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1065
        unsafe { env::remove_var(NL_OTEL_ENDPOINT) };
1066
        assert!(!any_per_signal_tls_variable_set());
1067
        let channels = load_balanced_channels()
1068
            .await
1069
            .expect("an unset endpoint is not an error");
1070
        assert!(channels.logs.is_none() && channels.traces.is_none() && channels.metrics.is_none());
1071
1072
        // An empty per-signal value is unset, so it does not split the
1073
        // channel either.
1074
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1075
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY", "") };
1076
        let split = any_per_signal_tls_variable_set();
1077
        clear_otlp_tls_env();
1078
        assert!(!split);
1079
1080
        if !dns_configured() {
1081
            eprintln!(
1082
                "Skipping the shared channel assertion: no DNS configuration \
1083
                 available (e.g. sandboxed Nix build)"
1084
            );
1085
            return;
1086
        }
1087
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1088
        unsafe { env::set_var(NL_OTEL_ENDPOINT, "http://localhost:4317") };
1089
        let channels = load_balanced_channels().await;
1090
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1091
        unsafe { env::remove_var(NL_OTEL_ENDPOINT) };
1092
        let channels = channels.expect("a valid endpoint builds");
1093
        assert!(channels.logs.is_some() && channels.traces.is_some() && channels.metrics.is_some());
1094
    }
1095
1096
    #[serial(env)]
1097
    #[nativelink_test("crate")]
1098
    async fn https_endpoint_without_variables_trusts_the_system_store() {
1099
        clear_otlp_tls_env();
1100
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1101
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "https://collector:4317") };
1102
        let logs = exporter_tls_config("LOGS");
1103
        let traces = exporter_tls_config("TRACES");
1104
        clear_otlp_tls_env();
1105
        assert!(
1106
            logs.unwrap().is_some(),
1107
            "an https endpoint gets a TLS config"
1108
        );
1109
        assert!(
1110
            traces.unwrap().is_none(),
1111
            "the default endpoint is plaintext"
1112
        );
1113
    }
1114
1115
    // An exporter handed the NL_OTEL_ENDPOINT channel sends on that channel,
1116
    // so the SDK endpoint it would otherwise use, and that endpoint's
1117
    // certificate variables, are not examined: a stale https endpoint with
1118
    // a missing CA file used to stop startup although nothing was going to
1119
    // connect to it.
1120
    #[serial(env)]
1121
    #[nativelink_test("crate")]
1122
    async fn supplied_channel_skips_the_sdk_endpoints_tls_configuration() {
1123
        clear_otlp_tls_env();
1124
        let dir = tempfile::tempdir().unwrap();
1125
        let missing = dir.path().join("missing.pem").to_str().unwrap().to_string();
1126
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1127
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "https://unused.example:4317") };
1128
        // SAFETY: `#[serial(env)]` serializes all env-var writes across tests.
1129
        unsafe { env::set_var("OTEL_EXPORTER_OTLP_CERTIFICATE", &missing) };
1130
        // Without a channel the exporter would connect to that endpoint, so
1131
        // its missing CA file is an error.
1132
        let own_connection = with_transport(SpanExporter::builder().with_tonic(), None, "TRACES");
1133
        let channel = if dns_configured() {
1134
            LoadBalancedChannel::builder(("localhost", 4317))
1135
                .channel()
1136
                .await
1137
                .ok()
1138
        } else {
1139
            eprintln!(
1140
                "Skipping the supplied-channel assertion: no DNS configuration \
1141
                 available (e.g. sandboxed Nix build)"
1142
            );
1143
            None
1144
        };
1145
0
        let supplied = channel.map(|channel| {
1146
0
            with_transport(
1147
0
                SpanExporter::builder().with_tonic(),
1148
0
                Some(channel),
1149
0
                "TRACES",
1150
            )
1151
0
        });
1152
        clear_otlp_tls_env();
1153
        assert_eq!(
1154
            own_connection
1155
                .expect_err("the missing CA file is an error for an exporter that would use it")
1156
                .code,
1157
            Code::InvalidArgument
1158
        );
1159
        if let Some(supplied) = supplied {
1160
            assert!(
1161
                supplied.is_ok(),
1162
                "a supplied channel must not read the ignored endpoint's variables: {:?}",
1163
                supplied.err()
1164
            );
1165
        }
1166
    }
1167
1168
    #[test]
1169
1
    fn inject_context_writes_baggage_into_request_metadata() {
1170
        use opentelemetry::baggage::BaggageExt;
1171
1172
1
        let propagator = BaggagePropagator::new();
1173
1
        let context =
1174
1
            opentelemetry::Context::new().with_baggage(vec![KeyValue::new(ENDUSER_ID, "alice")]);
1175
1
        let mut request = tonic::Request::new(());
1176
1
        inject_context_with(&propagator, &context, &mut request);
1177
1
        let baggage = request
1178
1
            .metadata()
1179
1
            .get("baggage")
1180
1
            .expect("the baggage header is written")
1181
1
            .to_str()
1182
1
            .unwrap();
1183
1
        assert!(baggage.contains("enduser.id=alice"), "{baggage}");
1184
1
    }
1185
1186
    #[test]
1187
1
    fn forward_client_headers_forwards_binary_header() {
1188
1
        let name = BAZEL_REQUESTMETADATA_HEADER;
1189
1
        let metadata = RequestMetadata {
1190
1
            tool_invocation_id: "inv-1".to_string(),
1191
1
            ..Default::default()
1192
1
        };
1193
1
        let encoded = BASE64_STANDARD_NO_PAD.encode(metadata.encode_to_vec());
1194
1
        let headers = ClientHeaders(Arc::new(HashMap::from([(name.to_string(), encoded)])));
1195
1
        let _guard = opentelemetry::Context::new().with_value(headers).attach();
1196
1197
1
        let mut request = tonic::Request::new(());
1198
1
        forward_client_headers(&mut request, &[name.to_string()]);
1199
1200
1
        let value = request
1201
1
            .metadata()
1202
1
            .get_bin(name)
1203
1
            .expect("the binary header is forwarded")
1204
1
            .to_bytes()
1205
1
            .unwrap();
1206
1
        let forwarded = RequestMetadata::decode(value.as_ref()).unwrap();
1207
1
        assert_eq!(forwarded.tool_invocation_id, "inv-1");
1208
1
    }
1209
}