1#![cfg(not(doctest))]
12#![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)] #[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#[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 }
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#[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 pub fn maximum_shutdown_duration(mut self, duration: Duration) -> Self {
161 self.maximum_shutdown_duration = Some(duration);
162 self
163 }
164
165 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 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, }
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 ..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 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
551enum 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#[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 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
913const 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 (
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 attributes.push(KeyValue::new(
1001 semconv::attribute::HTTP_HOST,
1002 "example.com:8080",
1003 ));
1004
1005 attributes.push(KeyValue::new(semcov::attribute::HTTP_METHOD, "POST"));
1007
1008 attributes.push(KeyValue::new(HTTP_PATH, "/path/12314/?q=ddds#123"));
1010
1011 attributes.push(KeyValue::new(
1013 semcov::attribute::HTTP_URL,
1014 "https://example.com:8080/webshop/articles/4?s=1",
1015 ));
1016
1017 attributes.push(KeyValue::new(
1019 semconv::attribute::HTTP_USER_AGENT,
1020 "CERN-LineMode/2.15 libwww/2.17b3",
1021 ));
1022
1023 attributes.push(KeyValue::new(semcov::attribute::HTTP_STATUS_CODE, 200i64));
1025
1026 attributes.push(KeyValue::new(
1028 semcov::trace::HTTP_ROUTE,
1029 "/webshop/articles/:article_id",
1030 ));
1031
1032 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 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}