Skip to main content

opentelemetry_stackdriver/
lib.rs

1//! # OpenTelemetry Stackdriver Exporter
2//!
3//! > **⚠️ DEPRECATED** — This crate is deprecated and will be removed from the
4//! > `opentelemetry-rust-contrib` repository. The crate has been unmaintained
5//! > and no committed maintainer stepped up in response to
6//! > [issue #609](https://github.com/open-telemetry/opentelemetry-rust-contrib/issues/609).
7//! > Users should migrate to OTLP — Google Cloud supports OTLP ingestion
8//! > directly, and the OpenTelemetry Collector ships a `googlecloud` exporter.
9//! > The last release of this crate on crates.io will remain available, but no
10//! > further releases are planned.
11#![cfg(not(doctest))]
12// unfortunately the proto code includes comments from the google proto files
13// that are interpreted as "doc tests" and will fail to build.
14// When this PR is merged we should be able to remove this attribute:
15// https://github.com/danburkert/prost/pull/291
16#![allow(
17    clippy::doc_lazy_continuation,
18    deprecated,
19    rustdoc::bare_urls,
20    rustdoc::broken_intra_doc_links,
21    rustdoc::invalid_rust_codeblocks
22)]
23#![deprecated(
24    since = "0.30.0",
25    note = "The `opentelemetry-stackdriver` crate is deprecated and will be removed from `opentelemetry-rust-contrib`. Migrate to OTLP (Google Cloud supports OTLP ingestion, and the OpenTelemetry Collector ships a `googlecloud` exporter). See https://github.com/open-telemetry/opentelemetry-rust-contrib/issues/609 for context."
26)]
27
28use std::{
29    borrow::Cow,
30    collections::HashMap,
31    fmt,
32    future::Future,
33    sync::{
34        atomic::{AtomicUsize, Ordering},
35        Arc, RwLock,
36    },
37    time::{Duration, Instant},
38};
39
40use futures_util::stream::StreamExt;
41use opentelemetry::{otel_error, trace::SpanId, Key, KeyValue, Value};
42use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult};
43use opentelemetry_sdk::{
44    trace::{SpanData, SpanExporter},
45    Resource,
46};
47use opentelemetry_semantic_conventions as semconv;
48use thiserror::Error;
49#[cfg(feature = "gcp-authorizer")]
50use tonic::metadata::MetadataValue;
51#[cfg(any(
52    feature = "tls-ring",
53    feature = "tls-native-roots",
54    feature = "tls-webpki-roots"
55))]
56use tonic::transport::ClientTlsConfig;
57use tonic::{transport::Channel, Code, Request};
58
59#[allow(clippy::derive_partial_eq_without_eq)] // tonic doesn't derive Eq for generated types
60#[allow(clippy::doc_overindented_list_items)]
61pub mod proto;
62
63#[cfg(feature = "propagator")]
64pub mod google_trace_context_propagator;
65
66use proto::devtools::cloudtrace::v2::span::time_event::Annotation;
67use proto::devtools::cloudtrace::v2::span::{
68    Attributes, Link, Links, SpanKind, TimeEvent, TimeEvents,
69};
70use proto::devtools::cloudtrace::v2::trace_service_client::TraceServiceClient;
71use proto::devtools::cloudtrace::v2::{
72    AttributeValue, BatchWriteSpansRequest, Span, TruncatableString,
73};
74use proto::logging::v2::{
75    log_entry::Payload, logging_service_v2_client::LoggingServiceV2Client, LogEntry,
76    LogEntrySourceLocation, WriteLogEntriesRequest,
77};
78use proto::rpc::Status;
79
80/// Exports opentelemetry tracing spans to Google StackDriver.
81///
82/// As of the time of this writing, the opentelemetry crate exposes no link information
83/// so this struct does not send link information.
84#[derive(Clone)]
85pub struct StackDriverExporter {
86    tx: futures_channel::mpsc::Sender<Vec<SpanData>>,
87    pending_count: Arc<AtomicUsize>,
88    maximum_shutdown_duration: Duration,
89    resource: Arc<RwLock<Option<Resource>>>,
90}
91
92impl StackDriverExporter {
93    pub fn builder() -> Builder {
94        Builder::default()
95    }
96
97    pub fn pending_count(&self) -> usize {
98        self.pending_count.load(Ordering::Relaxed)
99    }
100}
101
102impl SpanExporter for StackDriverExporter {
103    async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
104        match self.tx.clone().try_send(batch) {
105            Err(e) => Err(OTelSdkError::InternalFailure(format!("{e:?}"))),
106            Ok(()) => {
107                self.pending_count.fetch_add(1, Ordering::Relaxed);
108                Ok(())
109            }
110        }
111    }
112
113    fn shutdown(&self) -> OTelSdkResult {
114        let start = Instant::now();
115        while (Instant::now() - start) < self.maximum_shutdown_duration && self.pending_count() > 0
116        {
117            std::thread::yield_now();
118            // Spin for a bit and give the inner export some time to upload, with a timeout.
119        }
120        Ok(())
121    }
122
123    fn set_resource(&mut self, resource: &Resource) {
124        match self.resource.write() {
125            Ok(mut guard) => *guard = Some(resource.clone()),
126            Err(poisoned) => *poisoned.into_inner() = Some(resource.clone()),
127        }
128    }
129}
130
131impl fmt::Debug for StackDriverExporter {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        #[allow(clippy::unneeded_field_pattern)]
134        let Self {
135            tx: _,
136            pending_count,
137            maximum_shutdown_duration,
138            resource: _,
139        } = self;
140        f.debug_struct("StackDriverExporter")
141            .field("tx", &"(elided)")
142            .field("pending_count", pending_count)
143            .field("maximum_shutdown_duration", maximum_shutdown_duration)
144            .finish()
145    }
146}
147
148/// Helper type to build a `StackDriverExporter`.
149#[derive(Clone, Default)]
150pub struct Builder {
151    maximum_shutdown_duration: Option<Duration>,
152    num_concurrent_requests: Option<usize>,
153    log_context: Option<LogContext>,
154}
155
156impl Builder {
157    /// Set the maximum shutdown duration to export all the remaining data.
158    ///
159    /// If not set, defaults to 5 seconds.
160    pub fn maximum_shutdown_duration(mut self, duration: Duration) -> Self {
161        self.maximum_shutdown_duration = Some(duration);
162        self
163    }
164
165    /// Set the number of concurrent requests.
166    ///
167    /// If `num_concurrent_requests` is set to `0` or `None` then no limit is enforced.
168    pub fn num_concurrent_requests(mut self, num_concurrent_requests: usize) -> Self {
169        self.num_concurrent_requests = Some(num_concurrent_requests);
170        self
171    }
172
173    /// Enable writing log entries with the given `log_context`.
174    pub fn log_context(mut self, log_context: LogContext) -> Self {
175        self.log_context = Some(log_context);
176        self
177    }
178
179    pub async fn build<A: Authorizer>(
180        self,
181        authenticator: A,
182    ) -> Result<(StackDriverExporter, impl Future<Output = ()>), Error>
183    where
184        Error: From<A::Error>,
185    {
186        let Self {
187            maximum_shutdown_duration,
188            num_concurrent_requests,
189            log_context,
190        } = self;
191        let uri = http::uri::Uri::from_static("https://cloudtrace.googleapis.com:443");
192
193        #[cfg(any(
194            feature = "tls-ring",
195            feature = "tls-native-roots",
196            feature = "tls-webpki-roots"
197        ))]
198        let tls_config = ClientTlsConfig::new().with_enabled_roots();
199
200        let trace_channel_builder = Channel::builder(uri);
201        #[cfg(any(
202            feature = "tls-ring",
203            feature = "tls-native-roots",
204            feature = "tls-webpki-roots"
205        ))]
206        let trace_channel_builder = trace_channel_builder
207            .tls_config(tls_config.clone())
208            .map_err(|e| Error::Transport(e.into()))?;
209
210        let trace_channel = trace_channel_builder
211            .connect()
212            .await
213            .map_err(|e| Error::Transport(e.into()))?;
214
215        let log_client = match log_context {
216            Some(log_context) => {
217                let log_channel_builder = Channel::builder(http::uri::Uri::from_static(
218                    "https://logging.googleapis.com:443",
219                ));
220                #[cfg(any(
221                    feature = "tls-ring",
222                    feature = "tls-native-roots",
223                    feature = "tls-webpki-roots"
224                ))]
225                let log_channel_builder = log_channel_builder
226                    .tls_config(tls_config)
227                    .map_err(|e| Error::Transport(e.into()))?;
228
229                let log_channel = log_channel_builder
230                    .connect()
231                    .await
232                    .map_err(|e| Error::Transport(e.into()))?;
233
234                Some(LogClient {
235                    client: LoggingServiceV2Client::new(log_channel),
236                    context: Arc::new(InternalLogContext::from(log_context)),
237                })
238            }
239            None => None,
240        };
241
242        let (tx, rx) = futures_channel::mpsc::channel(64);
243        let pending_count = Arc::new(AtomicUsize::new(0));
244        let scopes = Arc::new(match log_client {
245            Some(_) => vec![TRACE_APPEND, LOGGING_WRITE],
246            None => vec![TRACE_APPEND],
247        });
248
249        let count_clone = pending_count.clone();
250        let resource = Arc::new(RwLock::new(None));
251        let ctx_resource = resource.clone();
252        let future = async move {
253            let trace_client = TraceServiceClient::new(trace_channel);
254            let authorizer = &authenticator;
255            let log_client = log_client.clone();
256            rx.for_each_concurrent(num_concurrent_requests, move |batch| {
257                let trace_client = trace_client.clone();
258                let log_client = log_client.clone();
259                let pending_count = count_clone.clone();
260                let scopes = scopes.clone();
261                let resource = ctx_resource.clone();
262                ExporterContext {
263                    trace_client,
264                    log_client,
265                    authorizer,
266                    pending_count,
267                    scopes,
268                    resource,
269                }
270                .export(batch)
271            })
272            .await
273        };
274
275        let exporter = StackDriverExporter {
276            tx,
277            pending_count,
278            maximum_shutdown_duration: maximum_shutdown_duration
279                .unwrap_or_else(|| Duration::from_secs(5)),
280            resource,
281        };
282
283        Ok((exporter, future))
284    }
285}
286
287struct ExporterContext<'a, A> {
288    trace_client: TraceServiceClient<Channel>,
289    log_client: Option<LogClient>,
290    authorizer: &'a A,
291    pending_count: Arc<AtomicUsize>,
292    scopes: Arc<Vec<&'static str>>,
293    resource: Arc<RwLock<Option<Resource>>>,
294}
295
296impl<A: Authorizer> ExporterContext<'_, A>
297where
298    Error: From<A::Error>,
299{
300    async fn export(mut self, batch: Vec<SpanData>) {
301        use proto::devtools::cloudtrace::v2::span::time_event::Value;
302
303        let mut entries = Vec::new();
304        let mut spans = Vec::with_capacity(batch.len());
305        for span in batch {
306            let trace_id = hex::encode(span.span_context.trace_id().to_bytes());
307            let span_id = hex::encode(span.span_context.span_id().to_bytes());
308            let time_event = match &self.log_client {
309                None => span
310                    .events
311                    .into_iter()
312                    .map(|event| TimeEvent {
313                        time: Some(event.timestamp.into()),
314                        value: Some(Value::Annotation(Annotation {
315                            description: Some(to_truncate(event.name.into_owned())),
316                            ..Default::default()
317                        })),
318                    })
319                    .collect(),
320                Some(client) => {
321                    entries.extend(span.events.into_iter().map(|event| {
322                        let (mut level, mut target, mut labels) =
323                            (LogSeverity::Default, None, HashMap::default());
324                        for kv in event.attributes {
325                            match kv.key.as_str() {
326                                "level" => {
327                                    level = match kv.value.as_str().as_ref() {
328                                        "DEBUG" | "TRACE" => LogSeverity::Debug,
329                                        "INFO" => LogSeverity::Info,
330                                        "WARN" => LogSeverity::Warning,
331                                        "ERROR" => LogSeverity::Error,
332                                        _ => LogSeverity::Default, // tracing::Level is limited to the above 5
333                                    }
334                                }
335                                "target" => target = Some(kv.value.as_str().into_owned()),
336                                key => {
337                                    labels.insert(key.to_owned(), kv.value.as_str().into_owned());
338                                }
339                            }
340                        }
341                        let project_id = self.authorizer.project_id();
342                        let log_id = &client.context.log_id;
343                        LogEntry {
344                            log_name: format!("projects/{project_id}/logs/{log_id}"),
345                            resource: Some(client.context.resource.clone()),
346                            severity: level as i32,
347                            timestamp: Some(event.timestamp.into()),
348                            labels,
349                            trace: format!("projects/{project_id}/traces/{trace_id}"),
350                            span_id: span_id.clone(),
351                            source_location: target.map(|target| LogEntrySourceLocation {
352                                file: String::new(),
353                                line: 0,
354                                function: target,
355                            }),
356                            payload: Some(Payload::TextPayload(event.name.into_owned())),
357                            // severity, source_location, text_payload
358                            ..Default::default()
359                        }
360                    }));
361
362                    vec![]
363                }
364            };
365
366            let resource = self.resource.read().ok();
367            let attributes = match resource {
368                Some(resource) => Attributes::new(span.attributes, resource.as_ref()),
369                None => Attributes::new(span.attributes, None),
370            };
371
372            spans.push(Span {
373                name: format!(
374                    "projects/{}/traces/{}/spans/{}",
375                    self.authorizer.project_id(),
376                    hex::encode(span.span_context.trace_id().to_bytes()),
377                    hex::encode(span.span_context.span_id().to_bytes())
378                ),
379                display_name: Some(to_truncate(span.name.into_owned())),
380                span_id: hex::encode(span.span_context.span_id().to_bytes()),
381                // From the API docs: If this is a root span,
382                // then this field must be empty.
383                parent_span_id: match span.parent_span_id {
384                    SpanId::INVALID => "".to_owned(),
385                    _ => hex::encode(span.parent_span_id.to_bytes()),
386                },
387                start_time: Some(span.start_time.into()),
388                end_time: Some(span.end_time.into()),
389                attributes: Some(attributes),
390                time_events: Some(TimeEvents {
391                    time_event,
392                    ..Default::default()
393                }),
394                links: transform_links(&span.links),
395                status: status(span.status),
396                span_kind: SpanKind::from(span.span_kind) as i32,
397                ..Default::default()
398            });
399        }
400
401        let mut req = Request::new(BatchWriteSpansRequest {
402            name: format!("projects/{}", self.authorizer.project_id()),
403            spans,
404        });
405
406        self.pending_count.fetch_sub(1, Ordering::Relaxed);
407        if let Err(e) = self.authorizer.authorize(&mut req, &self.scopes).await {
408            otel_error!(name: "ExportAuthorizeError", error = format!("{e:?}"));
409        } else if let Err(e) = self.trace_client.batch_write_spans(req).await {
410            otel_error!(name: "ExportTransportError", error = format!("{e:?}"));
411        }
412
413        let client = match &mut self.log_client {
414            Some(client) => client,
415            None => return,
416        };
417
418        let mut req = Request::new(WriteLogEntriesRequest {
419            log_name: format!(
420                "projects/{}/logs/{}",
421                self.authorizer.project_id(),
422                client.context.log_id,
423            ),
424            entries,
425            dry_run: false,
426            labels: HashMap::default(),
427            partial_success: true,
428            resource: None,
429        });
430
431        if let Err(e) = self.authorizer.authorize(&mut req, &self.scopes).await {
432            otel_error!(name: "ExportAuthorizeError", error = format!("{e:?}"));
433        } else if let Err(e) = client.client.write_log_entries(req).await {
434            otel_error!(name: "ExportTransportError", error =  format!("{e:?}"));
435        }
436    }
437}
438
439#[cfg(feature = "gcp-authorizer")]
440pub struct GcpAuthorizer {
441    provider: Arc<dyn gcp_auth::TokenProvider>,
442    project_id: Arc<str>,
443}
444
445#[cfg(feature = "gcp-authorizer")]
446impl GcpAuthorizer {
447    pub async fn new() -> Result<Self, Error> {
448        let provider = gcp_auth::provider()
449            .await
450            .map_err(|e| Error::Authorizer(e.into()))?;
451
452        let project_id = provider
453            .project_id()
454            .await
455            .map_err(|e| Error::Authorizer(e.into()))?;
456
457        Ok(Self {
458            provider,
459            project_id,
460        })
461    }
462    pub fn from_gcp_auth(provider: Arc<dyn gcp_auth::TokenProvider>, project_id: Arc<str>) -> Self {
463        Self {
464            provider,
465            project_id,
466        }
467    }
468}
469
470#[cfg(feature = "gcp-authorizer")]
471impl Authorizer for GcpAuthorizer {
472    type Error = Error;
473
474    fn project_id(&self) -> &str {
475        &self.project_id
476    }
477
478    async fn authorize<T: Send + Sync>(
479        &self,
480        req: &mut Request<T>,
481        scopes: &[&str],
482    ) -> Result<(), Self::Error> {
483        let token = self
484            .provider
485            .token(scopes)
486            .await
487            .map_err(|e| Error::Authorizer(e.into()))?;
488
489        req.metadata_mut().insert(
490            "authorization",
491            MetadataValue::try_from(format!("Bearer {}", token.as_str())).unwrap(),
492        );
493
494        Ok(())
495    }
496}
497
498pub trait Authorizer: Sync + Send + 'static {
499    type Error: std::error::Error + fmt::Debug + Send + Sync;
500
501    fn project_id(&self) -> &str;
502    fn authorize<T: Send + Sync>(
503        &self,
504        request: &mut Request<T>,
505        scopes: &[&str],
506    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
507}
508
509impl From<Value> for AttributeValue {
510    fn from(v: Value) -> AttributeValue {
511        use proto::devtools::cloudtrace::v2::attribute_value;
512        let new_value = match v {
513            Value::Bool(v) => attribute_value::Value::BoolValue(v),
514            Value::F64(v) => attribute_value::Value::StringValue(to_truncate(v.to_string())),
515            Value::I64(v) => attribute_value::Value::IntValue(v),
516            Value::String(v) => attribute_value::Value::StringValue(to_truncate(v.to_string())),
517            Value::Array(_) => attribute_value::Value::StringValue(to_truncate(v.to_string())),
518            _ => attribute_value::Value::StringValue(to_truncate("".to_string())),
519        };
520        AttributeValue {
521            value: Some(new_value),
522        }
523    }
524}
525
526fn to_truncate(s: String) -> TruncatableString {
527    TruncatableString {
528        value: s,
529        ..Default::default()
530    }
531}
532
533#[derive(Debug, Error)]
534pub enum Error {
535    #[error("authorizer error: {0}")]
536    Authorizer(#[source] Box<dyn std::error::Error + Send + Sync>),
537    #[error("I/O error: {0}")]
538    Io(#[from] std::io::Error),
539    #[error("{0}")]
540    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
541    #[error("tonic error: {0}")]
542    Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
543}
544
545impl opentelemetry_sdk::ExportError for Error {
546    fn exporter_name(&self) -> &'static str {
547        "stackdriver"
548    }
549}
550
551/// As defined in https://cloud.google.com/logging/docs/reference/v2/rpc/google.logging.type#google.logging.type.LogSeverity.
552enum LogSeverity {
553    Default = 0,
554    Debug = 100,
555    Info = 200,
556    Warning = 400,
557    Error = 500,
558}
559
560#[derive(Clone)]
561struct LogClient {
562    client: LoggingServiceV2Client<Channel>,
563    context: Arc<InternalLogContext>,
564}
565
566struct InternalLogContext {
567    log_id: String,
568    resource: proto::api::MonitoredResource,
569}
570
571#[derive(Clone)]
572pub struct LogContext {
573    pub log_id: String,
574    pub resource: MonitoredResource,
575}
576
577impl From<LogContext> for InternalLogContext {
578    fn from(cx: LogContext) -> Self {
579        let mut labels = HashMap::default();
580        let resource = match cx.resource {
581            MonitoredResource::AppEngine {
582                project_id,
583                module_id,
584                version_id,
585                zone,
586            } => {
587                labels.insert("project_id".to_string(), project_id);
588                if let Some(module_id) = module_id {
589                    labels.insert("module_id".to_string(), module_id);
590                }
591                if let Some(version_id) = version_id {
592                    labels.insert("version_id".to_string(), version_id);
593                }
594                if let Some(zone) = zone {
595                    labels.insert("zone".to_string(), zone);
596                }
597
598                proto::api::MonitoredResource {
599                    r#type: "gae_app".to_owned(),
600                    labels,
601                }
602            }
603            MonitoredResource::CloudFunction {
604                project_id,
605                function_name,
606                region,
607            } => {
608                labels.insert("project_id".to_string(), project_id);
609                if let Some(function_name) = function_name {
610                    labels.insert("function_name".to_string(), function_name);
611                }
612                if let Some(region) = region {
613                    labels.insert("region".to_string(), region);
614                }
615
616                proto::api::MonitoredResource {
617                    r#type: "cloud_function".to_owned(),
618                    labels,
619                }
620            }
621            MonitoredResource::CloudRunJob {
622                project_id,
623                job_name,
624                location,
625            } => {
626                labels.insert("project_id".to_string(), project_id);
627                if let Some(job_name) = job_name {
628                    labels.insert("job_name".to_string(), job_name);
629                }
630                if let Some(location) = location {
631                    labels.insert("location".to_string(), location);
632                }
633
634                proto::api::MonitoredResource {
635                    r#type: "cloud_run_job".to_owned(),
636                    labels,
637                }
638            }
639            MonitoredResource::CloudRunRevision {
640                project_id,
641                service_name,
642                revision_name,
643                location,
644                configuration_name,
645            } => {
646                labels.insert("project_id".to_string(), project_id);
647                if let Some(service_name) = service_name {
648                    labels.insert("service_name".to_string(), service_name);
649                }
650                if let Some(revision_name) = revision_name {
651                    labels.insert("revision_name".to_string(), revision_name);
652                }
653                if let Some(location) = location {
654                    labels.insert("location".to_string(), location);
655                }
656                if let Some(configuration_name) = configuration_name {
657                    labels.insert("configuration_name".to_string(), configuration_name);
658                }
659
660                proto::api::MonitoredResource {
661                    r#type: "cloud_run_revision".to_owned(),
662                    labels,
663                }
664            }
665
666            MonitoredResource::ComputeEngine {
667                project_id,
668                instance_id,
669                zone,
670            } => {
671                labels.insert("project_id".to_string(), project_id);
672                if let Some(instance_id) = instance_id {
673                    labels.insert("instance_id".to_string(), instance_id);
674                }
675                if let Some(zone) = zone {
676                    labels.insert("zone".to_string(), zone);
677                }
678
679                proto::api::MonitoredResource {
680                    r#type: "gce_instance".to_owned(),
681                    labels,
682                }
683            }
684
685            MonitoredResource::GenericNode {
686                project_id,
687                location,
688                namespace,
689                node_id,
690            } => {
691                labels.insert("project_id".to_string(), project_id);
692                if let Some(location) = location {
693                    labels.insert("location".to_string(), location);
694                }
695                if let Some(namespace) = namespace {
696                    labels.insert("namespace".to_string(), namespace);
697                }
698                if let Some(node_id) = node_id {
699                    labels.insert("node_id".to_string(), node_id);
700                }
701
702                proto::api::MonitoredResource {
703                    r#type: "generic_node".to_owned(),
704                    labels,
705                }
706            }
707            MonitoredResource::GenericTask {
708                project_id,
709                location,
710                namespace,
711                job,
712                task_id,
713            } => {
714                labels.insert("project_id".to_owned(), project_id);
715                if let Some(location) = location {
716                    labels.insert("location".to_owned(), location);
717                }
718                if let Some(namespace) = namespace {
719                    labels.insert("namespace".to_owned(), namespace);
720                }
721                if let Some(job) = job {
722                    labels.insert("job".to_owned(), job);
723                }
724                if let Some(task_id) = task_id {
725                    labels.insert("task_id".to_owned(), task_id);
726                }
727
728                proto::api::MonitoredResource {
729                    r#type: "generic_task".to_owned(),
730                    labels,
731                }
732            }
733            MonitoredResource::Global { project_id } => {
734                labels.insert("project_id".to_owned(), project_id);
735                proto::api::MonitoredResource {
736                    r#type: "global".to_owned(),
737                    labels,
738                }
739            }
740            MonitoredResource::KubernetesEngine {
741                project_id,
742                cluster_name,
743                location,
744                pod_name,
745                namespace_name,
746                container_name,
747            } => {
748                labels.insert("project_id".to_string(), project_id);
749                if let Some(cluster_name) = cluster_name {
750                    labels.insert("cluster_name".to_string(), cluster_name);
751                }
752                if let Some(location) = location {
753                    labels.insert("location".to_string(), location);
754                }
755                if let Some(pod_name) = pod_name {
756                    labels.insert("pod_name".to_string(), pod_name);
757                }
758                if let Some(namespace_name) = namespace_name {
759                    labels.insert("namespace_name".to_string(), namespace_name);
760                }
761                if let Some(container_name) = container_name {
762                    labels.insert("container_name".to_string(), container_name);
763                }
764
765                proto::api::MonitoredResource {
766                    r#type: "k8s_container".to_owned(),
767                    labels,
768                }
769            }
770        };
771
772        Self {
773            log_id: cx.log_id,
774            resource,
775        }
776    }
777}
778
779/// A description of a `MonitoredResource`.
780///
781/// Possible values are listed in the [API documentation](https://cloud.google.com/logging/docs/api/v2/resource-list).
782/// Please submit an issue or pull request if you want to use a resource type not listed here.
783#[derive(Clone)]
784pub enum MonitoredResource {
785    AppEngine {
786        project_id: String,
787        module_id: Option<String>,
788        version_id: Option<String>,
789        zone: Option<String>,
790    },
791    CloudFunction {
792        project_id: String,
793        function_name: Option<String>,
794        region: Option<String>,
795    },
796    CloudRunJob {
797        project_id: String,
798        job_name: Option<String>,
799        location: Option<String>,
800    },
801    CloudRunRevision {
802        project_id: String,
803        service_name: Option<String>,
804        revision_name: Option<String>,
805        location: Option<String>,
806        configuration_name: Option<String>,
807    },
808    ComputeEngine {
809        project_id: String,
810        instance_id: Option<String>,
811        zone: Option<String>,
812    },
813    KubernetesEngine {
814        project_id: String,
815        location: Option<String>,
816        cluster_name: Option<String>,
817        namespace_name: Option<String>,
818        pod_name: Option<String>,
819        container_name: Option<String>,
820    },
821    GenericNode {
822        project_id: String,
823        location: Option<String>,
824        namespace: Option<String>,
825        node_id: Option<String>,
826    },
827    GenericTask {
828        project_id: String,
829        location: Option<String>,
830        namespace: Option<String>,
831        job: Option<String>,
832        task_id: Option<String>,
833    },
834    Global {
835        project_id: String,
836    },
837}
838
839impl Attributes {
840    /// Combines `EvictedHashMap` and `Resource` attributes into a maximum of 32.
841    ///
842    /// The `Resource` takes precedence over the `EvictedHashMap` attributes.
843    fn new(attributes: Vec<KeyValue>, resource: Option<&Resource>) -> Self {
844        let mut new = Self {
845            dropped_attributes_count: 0,
846            attribute_map: HashMap::with_capacity(Ord::min(
847                MAX_ATTRIBUTES_PER_SPAN,
848                attributes.len() + resource.map_or(0, |r| r.len()),
849            )),
850        };
851
852        if let Some(resource) = resource {
853            for (k, v) in resource.iter() {
854                new.push(Cow::Borrowed(k), Cow::Borrowed(v));
855            }
856        }
857
858        for kv in attributes {
859            new.push(Cow::Owned(kv.key), Cow::Owned(kv.value));
860        }
861
862        new
863    }
864
865    fn push(&mut self, key: Cow<'_, Key>, value: Cow<'_, Value>) {
866        if self.attribute_map.len() >= MAX_ATTRIBUTES_PER_SPAN {
867            self.dropped_attributes_count += 1;
868            return;
869        }
870
871        let key_str = key.as_str();
872        if key_str.len() > 128 {
873            self.dropped_attributes_count += 1;
874            return;
875        }
876
877        for (otel_key, gcp_key) in KEY_MAP {
878            if otel_key == key_str {
879                self.attribute_map
880                    .insert(gcp_key.to_owned(), value.into_owned().into());
881                return;
882            }
883        }
884
885        self.attribute_map.insert(
886            match key {
887                Cow::Owned(k) => k.to_string(),
888                Cow::Borrowed(k) => k.to_string(),
889            },
890            value.into_owned().into(),
891        );
892    }
893}
894
895fn transform_links(links: &opentelemetry_sdk::trace::SpanLinks) -> Option<Links> {
896    if links.is_empty() {
897        return None;
898    }
899
900    Some(Links {
901        dropped_links_count: links.dropped_count as i32,
902        link: links
903            .iter()
904            .map(|link| Link {
905                trace_id: hex::encode(link.span_context.trace_id().to_bytes()),
906                span_id: hex::encode(link.span_context.span_id().to_bytes()),
907                ..Default::default()
908            })
909            .collect(),
910    })
911}
912
913// Map conventional OpenTelemetry keys to their GCP counterparts.
914//
915// https://cloud.google.com/trace/docs/trace-labels
916const KEY_MAP: [(&str, &str); 19] = [
917    (HTTP_PATH, GCP_HTTP_PATH),
918    (semconv::attribute::HTTP_HOST, "/http/host"),
919    ("http.request.header.host", "/http/host"),
920    (semconv::attribute::HTTP_METHOD, "/http/method"),
921    (semconv::attribute::HTTP_REQUEST_METHOD, "/http/method"),
922    (semconv::attribute::HTTP_TARGET, "/http/path"),
923    (semconv::attribute::URL_PATH, "/http/path"),
924    (semconv::attribute::HTTP_URL, "/http/url"),
925    (semconv::attribute::URL_FULL, "/http/url"),
926    (semconv::attribute::HTTP_USER_AGENT, "/http/user_agent"),
927    (semconv::attribute::USER_AGENT_ORIGINAL, "/http/user_agent"),
928    (semconv::attribute::HTTP_STATUS_CODE, "/http/status_code"),
929    // https://cloud.google.com/trace/docs/trace-labels#canonical-gke
930    (
931        semconv::attribute::HTTP_RESPONSE_STATUS_CODE,
932        "/http/status_code",
933    ),
934    (
935        semconv::attribute::K8S_CLUSTER_NAME,
936        "g.co/r/k8s_container/cluster_name",
937    ),
938    (
939        semconv::attribute::K8S_NAMESPACE_NAME,
940        "g.co/r/k8s_container/namespace",
941    ),
942    (
943        semconv::attribute::K8S_POD_NAME,
944        "g.co/r/k8s_container/pod_name",
945    ),
946    (
947        semconv::attribute::K8S_CONTAINER_NAME,
948        "g.co/r/k8s_container/container_name",
949    ),
950    (semconv::trace::HTTP_ROUTE, "/http/route"),
951    (HTTP_PATH, GCP_HTTP_PATH),
952];
953
954const HTTP_PATH: &str = "http.path";
955const GCP_HTTP_PATH: &str = "/http/path";
956
957impl From<opentelemetry::trace::SpanKind> for SpanKind {
958    fn from(span_kind: opentelemetry::trace::SpanKind) -> Self {
959        match span_kind {
960            opentelemetry::trace::SpanKind::Client => SpanKind::Client,
961            opentelemetry::trace::SpanKind::Server => SpanKind::Server,
962            opentelemetry::trace::SpanKind::Producer => SpanKind::Producer,
963            opentelemetry::trace::SpanKind::Consumer => SpanKind::Consumer,
964            opentelemetry::trace::SpanKind::Internal => SpanKind::Internal,
965        }
966    }
967}
968
969fn status(value: opentelemetry::trace::Status) -> Option<Status> {
970    match value {
971        opentelemetry::trace::Status::Ok => Some(Status {
972            code: Code::Ok as i32,
973            message: "".to_owned(),
974            details: vec![],
975        }),
976        opentelemetry::trace::Status::Unset => None,
977        opentelemetry::trace::Status::Error { description } => Some(Status {
978            code: Code::Unknown as i32,
979            message: description.into(),
980            details: vec![],
981        }),
982    }
983}
984const TRACE_APPEND: &str = "https://www.googleapis.com/auth/trace.append";
985const LOGGING_WRITE: &str = "https://www.googleapis.com/auth/logging.write";
986const MAX_ATTRIBUTES_PER_SPAN: usize = 32;
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991    use opentelemetry::{KeyValue, Value};
992    use opentelemetry_semantic_conventions as semcov;
993
994    #[test]
995    fn test_attributes_mapping() {
996        let capacity = 10;
997        let mut attributes = Vec::with_capacity(capacity);
998
999        //	hostAttribute       = "http.host"
1000        attributes.push(KeyValue::new(
1001            semconv::attribute::HTTP_HOST,
1002            "example.com:8080",
1003        ));
1004
1005        // 	methodAttribute     = "http.method"
1006        attributes.push(KeyValue::new(semcov::attribute::HTTP_METHOD, "POST"));
1007
1008        // 	pathAttribute       = "http.path"
1009        attributes.push(KeyValue::new(HTTP_PATH, "/path/12314/?q=ddds#123"));
1010
1011        // 	urlAttribute        = "http.url"
1012        attributes.push(KeyValue::new(
1013            semcov::attribute::HTTP_URL,
1014            "https://example.com:8080/webshop/articles/4?s=1",
1015        ));
1016
1017        // 	userAgentAttribute  = "http.user_agent"
1018        attributes.push(KeyValue::new(
1019            semconv::attribute::HTTP_USER_AGENT,
1020            "CERN-LineMode/2.15 libwww/2.17b3",
1021        ));
1022
1023        // 	statusCodeAttribute = "http.status_code"
1024        attributes.push(KeyValue::new(semcov::attribute::HTTP_STATUS_CODE, 200i64));
1025
1026        // 	statusCodeAttribute = "http.route"
1027        attributes.push(KeyValue::new(
1028            semcov::trace::HTTP_ROUTE,
1029            "/webshop/articles/:article_id",
1030        ));
1031
1032        // 	serviceAttribute    = "service.name"
1033        let resources = Resource::builder_empty()
1034            .with_attributes([KeyValue::new(
1035                semcov::resource::SERVICE_NAME,
1036                "Test Service Name",
1037            )])
1038            .build();
1039
1040        let actual = Attributes::new(attributes, Some(&resources));
1041        assert_eq!(actual.attribute_map.len(), 8);
1042        assert_eq!(actual.dropped_attributes_count, 0);
1043        assert_eq!(
1044            actual.attribute_map.get("/http/host"),
1045            Some(&AttributeValue::from(Value::String(
1046                "example.com:8080".into()
1047            )))
1048        );
1049        assert_eq!(
1050            actual.attribute_map.get("/http/method"),
1051            Some(&AttributeValue::from(Value::String("POST".into()))),
1052        );
1053        assert_eq!(
1054            actual.attribute_map.get("/http/path"),
1055            Some(&AttributeValue::from(Value::String(
1056                "/path/12314/?q=ddds#123".into()
1057            ))),
1058        );
1059        assert_eq!(
1060            actual.attribute_map.get("/http/route"),
1061            Some(&AttributeValue::from(Value::String(
1062                "/webshop/articles/:article_id".into()
1063            ))),
1064        );
1065        assert_eq!(
1066            actual.attribute_map.get("/http/url"),
1067            Some(&AttributeValue::from(Value::String(
1068                "https://example.com:8080/webshop/articles/4?s=1".into(),
1069            ))),
1070        );
1071        assert_eq!(
1072            actual.attribute_map.get("/http/user_agent"),
1073            Some(&AttributeValue::from(Value::String(
1074                "CERN-LineMode/2.15 libwww/2.17b3".into()
1075            ))),
1076        );
1077        assert_eq!(
1078            actual.attribute_map.get("/http/status_code"),
1079            Some(&AttributeValue::from(Value::I64(200))),
1080        );
1081    }
1082
1083    #[test]
1084    fn test_too_many() {
1085        let resources = Resource::builder_empty()
1086            .with_attributes([KeyValue::new(
1087                semconv::attribute::USER_AGENT_ORIGINAL,
1088                "Test Service Name UA",
1089            )])
1090            .build();
1091        let mut attributes = Vec::with_capacity(32);
1092        for i in 0..32 {
1093            attributes.push(KeyValue::new(
1094                format!("key{i}"),
1095                Value::String(format!("value{i}").into()),
1096            ));
1097        }
1098
1099        let actual = Attributes::new(attributes, Some(&resources));
1100        assert_eq!(actual.attribute_map.len(), 32);
1101        assert_eq!(actual.dropped_attributes_count, 1);
1102        assert_eq!(
1103            actual.attribute_map.get("/http/user_agent"),
1104            Some(&AttributeValue::from(Value::String(
1105                "Test Service Name UA".into()
1106            ))),
1107        );
1108    }
1109
1110    #[test]
1111    fn test_attributes_mapping_http_target() {
1112        let attributes = vec![KeyValue::new(
1113            semcov::attribute::HTTP_TARGET,
1114            "/path/12314/?q=ddds#123",
1115        )];
1116
1117        //	hostAttribute       = "http.target"
1118
1119        let resources = Resource::builder_empty().with_attributes([]).build();
1120        let actual = Attributes::new(attributes, Some(&resources));
1121        assert_eq!(actual.attribute_map.len(), 1);
1122        assert_eq!(actual.dropped_attributes_count, 0);
1123        assert_eq!(
1124            actual.attribute_map.get("/http/path"),
1125            Some(&AttributeValue::from(Value::String(
1126                "/path/12314/?q=ddds#123".into()
1127            ))),
1128        );
1129    }
1130
1131    #[test]
1132    fn test_attributes_mapping_dropped_attributes_count() {
1133        let attributes = vec![KeyValue::new("answer", Value::I64(42)),KeyValue::new("long_attribute_key_dvwmacxpeefbuemoxljmqvldjxmvvihoeqnuqdsyovwgljtnemouidabhkmvsnauwfnaihekcfwhugejboiyfthyhmkpsaxtidlsbwsmirebax", Value::String("Some value".into()))];
1134
1135        let resources = Resource::builder_empty().with_attributes([]).build();
1136        let actual = Attributes::new(attributes, Some(&resources));
1137        assert_eq!(
1138            actual,
1139            Attributes {
1140                attribute_map: HashMap::from([(
1141                    "answer".into(),
1142                    AttributeValue::from(Value::I64(42))
1143                ),]),
1144                dropped_attributes_count: 1,
1145            }
1146        );
1147        assert_eq!(actual.attribute_map.len(), 1);
1148        assert_eq!(actual.dropped_attributes_count, 1);
1149    }
1150}