From c8f620882792e88f02d28a079c8eecac0772a4bf Mon Sep 17 00:00:00 2001 From: Jan Kuehle Date: Wed, 8 May 2024 21:00:06 +0200 Subject: [PATCH 1/2] Fix tag extraction from resource attributes Change how the tags ai.cloud.role, ai.cloud.roleInstance, ai.application.ver and ai.internal.sdkVersion are extracted: - Spans no longer extract them from span attributes. They still extract them from resource attributes. And they newly extract them also from instrumentation library attributes. - Events newly extract them from resource and instrumentation library attributes. In addition metrics no longer extract the tag ai.user.authUserId from the enduser.id attribute. --- CHANGELOG.md | 7 ++ src/lib.rs | 24 +++-- src/quick_pulse.rs | 4 +- src/tags.rs | 92 ++++++++++--------- .../http_requests__live_metrics.snap | 2 + .../http_requests__traces_simple.snap | 3 + 6 files changed, 82 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e42ac..16b969b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +- Change how the tags Could role, Cloud role instance, Application version and Internal SDK version are extracted: + + - Spans no longer extract them from span attributes. They still extract them from resource attributes. And they newly extract them also from instrumentation library attributes. + - Events newly extract them from resource and instrumentation library attributes. + + In addition metrics no longer extract the tag Authenticated user id from the enduser.id attribute. + ## [0.30.0] - 2024-03-08 - Upgrade `opentelemetry` and `opentelemetry_sdk` to `v0.22`. diff --git a/src/lib.rs b/src/lib.rs index eb74fbd..108d10b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,6 +160,21 @@ async fn main() { //! - [OpenTelemetry specification: Span](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#span) //! - [Application Insights data model](https://docs.microsoft.com/en-us/azure/azure-monitor/app/data-model) //! +//! ## Resource +//! +//! Resource and instrumentation library attributes map to the following fields for spans, events +//! and metrics (the mapping tries to follow the OpenTelemetry semantic conventions for [resource]). +//! +//! [resource]: https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/resource/semantic_conventions +//! +//! | OpenTelemetry attribute key | Application Insights field | +//! | ---------------------------------------------- | -------------------------------------------------------- | +//! | `service.namespace` + `service.name` | Context: Cloud role (`ai.cloud.role`) | +//! | `service.instance.id` | Context: Cloud role instance (`ai.cloud.roleInstance`) | +//! | `service.version` | Context: Application version (`ai.application.ver`) | +//! | `telemetry.sdk.name` + `telemetry.sdk.version` | Context: Internal SDK version (`ai.internal.sdkVersion`) | +//! | `ai.*` | Context: AppInsights Tag (`ai.*`) | +//! //! ## Spans //! //! The OpenTelemetry SpanKind determines the Application Insights telemetry type: @@ -173,22 +188,17 @@ async fn main() { //! the status `Error`; otherwise `true`. //! //! The following of the Span's attributes map to special fields in Application Insights (the -//! mapping tries to follow the OpenTelemetry semantic conventions for [trace] and [resource]). +//! mapping tries to follow the OpenTelemetry semantic conventions for [trace]). //! //! Note: for `INTERNAL` Spans the Dependency Type is always `"InProc"`. //! //! [trace]: https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/trace/semantic_conventions -//! [resource]: https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/resource/semantic_conventions //! [Dependency]: https://learn.microsoft.com/en-us/azure/azure-monitor/app/data-model-dependency-telemetry //! [Request]: https://learn.microsoft.com/en-us/azure/azure-monitor/app/data-model-request-telemetry //! //! | OpenTelemetry attribute key | Application Insights field | -//! | -------------------------------------------------------------------------- | ----------------------------------------------------- | -//! | `service.version` | Context: Application version (`ai.application.ver`) | +//! | -------------------------------------------------------------------------- | -------------------------------------------------------- | //! | `enduser.id` | Context: Authenticated user id (`ai.user.authUserId`) | -//! | `service.namespace` + `service.name` | Context: Cloud role (`ai.cloud.role`) | -//! | `service.instance.id` | Context: Cloud role instance (`ai.cloud.roleInstance`) | -//! | `telemetry.sdk.name` + `telemetry.sdk.version` | Context: Internal SDK version (`ai.internal.sdkVersion`) | //! | `SpanKind::Server` + `http.request.method` + `http.route` | Context: Operation Name (`ai.operation.name`) | //! | `ai.*` | Context: AppInsights Tag (`ai.*`) | //! | `url.full` | Dependency Data | diff --git a/src/quick_pulse.rs b/src/quick_pulse.rs index 2042f26..09d25e6 100644 --- a/src/quick_pulse.rs +++ b/src/quick_pulse.rs @@ -1,6 +1,6 @@ use crate::{ models::{context_tag_keys, QuickPulseEnvelope, QuickPulseMetric}, - tags::get_tags_from_attrs, + tags::get_tags_for_resource, trace::{get_duration, is_remote_dependency_success, is_request_success, EVENT_NAME_EXCEPTION}, uploader_quick_pulse::{self, PostOrPing}, Error, @@ -170,7 +170,7 @@ impl QuickPulseSender { instrumentation_key: String, resource: Resource, ) -> Self { - let mut tags = get_tags_from_attrs(resource.iter()); + let mut tags = get_tags_for_resource(&resource); let machine_name = resource .get(Key::from_static_str(semcov::resource::HOST_NAME)) .map(|v| v.as_str().into_owned()) diff --git a/src/tags.rs b/src/tags.rs index 88e7e9c..42a13dc 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,21 +1,21 @@ use crate::models::context_tag_keys::{self as tags, Tags, TAG_KEY_LOOKUP}; -#[cfg(feature = "metrics")] -use opentelemetry::InstrumentationLibrary; use opentelemetry::{ trace::{SpanId, SpanKind}, - Key, Value, + InstrumentationLibrary, Key, Value, }; -use opentelemetry_sdk::export::trace::SpanData; #[cfg(feature = "metrics")] -use opentelemetry_sdk::{AttributeSet, Resource}; +use opentelemetry_sdk::AttributeSet; +use opentelemetry_sdk::{export::trace::SpanData, Resource}; use opentelemetry_semantic_conventions as semcov; use std::collections::HashMap; pub(crate) fn get_tags_for_span(span: &SpanData) -> Tags { - let mut tags = get_tags_from_attrs( - span.resource - .iter() - .chain(span.attributes.iter().map(|kv| (&kv.key, &kv.value))), + let mut tags = Tags::new(); + build_tags_from_resource_attrs(&mut tags, &span.resource, &span.instrumentation_lib); + + let attrs_map = build_tags_from_attrs( + &mut tags, + span.attributes.iter().map(|kv| (&kv.key, &kv.value)), ); // Set the operation id and operation parent id. @@ -24,21 +24,21 @@ pub(crate) fn get_tags_for_span(span: &SpanData) -> Tags { tags.insert(tags::OPERATION_PARENT_ID, span.parent_span_id.to_string()); } + if let Some(user_id) = attrs_map.get(semcov::trace::ENDUSER_ID) { + // Using authenticated user id here to be safe. Or would ai.user.id (anonymous user id) + // fit better? + tags.insert(tags::USER_AUTH_USER_ID, user_id.as_str().into_owned()); + } + // Ensure the name of the operation is `METHOD /the/route/path`. if span.span_kind == SpanKind::Server || span.span_kind == SpanKind::Consumer { - let mut method: Option<&Value> = None; - let mut route: Option<&Value> = None; - for kv in &span.attributes { - #[allow(deprecated)] - if kv.key.as_str() == semcov::trace::HTTP_REQUEST_METHOD - || kv.key.as_str() == semcov::trace::HTTP_METHOD - { - method = Some(&kv.value); - } else if kv.key.as_str() == semcov::trace::HTTP_ROUTE { - route = Some(&kv.value); - } - } - + let method = attrs_map + .get(semcov::trace::HTTP_REQUEST_METHOD) + .or_else(|| { + #[allow(deprecated)] + attrs_map.get(semcov::trace::HTTP_METHOD) + }); + let route = attrs_map.get(semcov::trace::HTTP_ROUTE); if let (Some(method), Some(route)) = (method, route) { tags.insert( tags::OPERATION_NAME, @@ -52,6 +52,8 @@ pub(crate) fn get_tags_for_span(span: &SpanData) -> Tags { pub(crate) fn get_tags_for_event(span: &SpanData) -> Tags { let mut tags = Tags::new(); + build_tags_from_resource_attrs(&mut tags, &span.resource, &span.instrumentation_lib); + tags.insert(tags::OPERATION_ID, span.span_context.trace_id().to_string()); tags.insert( tags::OPERATION_PARENT_ID, @@ -66,23 +68,23 @@ pub(crate) fn get_tags_for_metric( scope: &InstrumentationLibrary, attrs: &AttributeSet, ) -> Tags { - get_tags_from_attrs( - resource.iter().chain( - scope - .attributes - .iter() - .map(|kv| (&kv.key, &kv.value)) - .chain(attrs.iter()), - ), - ) + let mut tags = Tags::new(); + build_tags_from_resource_attrs(&mut tags, resource, scope); + build_tags_from_attrs(&mut tags, attrs.iter()); + tags +} + +#[cfg(feature = "live-metrics")] +pub(crate) fn get_tags_for_resource(resource: &Resource) -> Tags { + let mut tags = Tags::new(); + build_tags_from_resource_attrs(&mut tags, resource, &Default::default()); + tags } -pub(crate) fn get_tags_from_attrs<'a, T>(attrs: T) -> Tags +fn build_tags_from_attrs<'a, T>(tags: &mut Tags, attrs: T) -> HashMap<&'a str, &'a Value> where T: IntoIterator, { - let mut tags = Tags::new(); - let mut attrs_map: HashMap<_, _> = HashMap::new(); for (k, v) in attrs.into_iter() { // First, allow the user to explicitly express tags with attributes that start with `ai.` @@ -99,11 +101,21 @@ where attrs_map.insert(k, v); } - if let Some(user_id) = attrs_map.get(semcov::trace::ENDUSER_ID) { - // Using authenticated user id here to be safe. Or would ai.user.id (anonymous user id) - // fit better? - tags.insert(tags::USER_AUTH_USER_ID, user_id.as_str().into_owned()); - } + attrs_map +} + +fn build_tags_from_resource_attrs( + tags: &mut Tags, + resource: &Resource, + instrumentation_lib: &InstrumentationLibrary, +) { + let attrs = resource.iter().chain( + instrumentation_lib + .attributes + .iter() + .map(|kv| (&kv.key, &kv.value)), + ); + let attrs_map = build_tags_from_attrs(tags, attrs); if let Some(service_name) = attrs_map.get(semcov::resource::SERVICE_NAME) { let mut cloud_role = service_name.as_str().into_owned(); @@ -139,6 +151,4 @@ where format!("{}:{}", sdk_name.as_str(), sdk_version), ); } - - tags } diff --git a/tests/snapshots/http_requests__live_metrics.snap b/tests/snapshots/http_requests__live_metrics.snap index 6da68a4..d1076d7 100644 --- a/tests/snapshots/http_requests__live_metrics.snap +++ b/tests/snapshots/http_requests__live_metrics.snap @@ -200,6 +200,8 @@ content-encoding: gzip "name": "Microsoft.ApplicationInsights.Exception", "sampleRate": 100.0, "tags": { + "ai.cloud.role": "unknown_service", + "ai.internal.sdkVersion": "opentelemetry:0.22.1", "ai.operation.id": "STRIPPED", "ai.operation.parentId": "STRIPPED" }, diff --git a/tests/snapshots/http_requests__traces_simple.snap b/tests/snapshots/http_requests__traces_simple.snap index 3e6c8a0..3745741 100644 --- a/tests/snapshots/http_requests__traces_simple.snap +++ b/tests/snapshots/http_requests__traces_simple.snap @@ -68,6 +68,7 @@ content-encoding: gzip "name": "Microsoft.ApplicationInsights.Message", "sampleRate": 100.0, "tags": { + "ai.cloud.role": "test.server", "ai.operation.id": "STRIPPED", "ai.operation.parentId": "STRIPPED" }, @@ -88,6 +89,7 @@ content-encoding: gzip "name": "Microsoft.ApplicationInsights.Event", "sampleRate": 100.0, "tags": { + "ai.cloud.role": "test.server", "ai.operation.id": "STRIPPED", "ai.operation.parentId": "STRIPPED" }, @@ -110,6 +112,7 @@ content-encoding: gzip "name": "Microsoft.ApplicationInsights.Exception", "sampleRate": 100.0, "tags": { + "ai.cloud.role": "test.server", "ai.operation.id": "STRIPPED", "ai.operation.parentId": "STRIPPED" }, From 56a5bd898a7bba176164d3dfe546f5cd2990bf6c Mon Sep 17 00:00:00 2001 From: Jan Kuehle Date: Wed, 8 May 2024 21:34:59 +0200 Subject: [PATCH 2/2] Enable tokio multi thread runtime for tests --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e497d04..c6ad3f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ rand = "0.8.5" regex = "1.5.5" reqwest = { version = "0.11", default-features = false, features = ["blocking"] } test-case = "3.0.0" -tokio = { version = "1.17.0", features = ["rt", "macros", "process", "time"] } +tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros", "process", "time"] } version-sync = { version = "0.9.4", default-features = false, features = ["html_root_url_updated", "contains_regex"] } [badges]