Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,15 @@ The extension creates the following histogram metrics for each Lambda invocation
| `dash0.faas.billed_duration` | ms | Billed duration of the invocation. |
| `faas.mem_usage` | MB | Memory used by the invocation. |

#### Metric Attributes

The following attributes are added to each metric data point:

| Attribute | Type | Description |
|---|---|---|
| `cloud.resource_id` | string | The full ARN of the Lambda function. |
| `cloud.account.id` | string | The AWS account ID. |

#### Resource Attributes (Metrics)

These attributes are added to the resource of metric data:

| Attribute | Type | Description |
|---|---|---|
| `cloud.platform` | string | Always set to `aws_lambda`. |
| `cloud.resource.id` | string | The full ARN of the Lambda function. |
Comment thread
mosheshaham-dash0 marked this conversation as resolved.
Outdated
| `cloud.account.id` | string | The AWS account ID. |
| `service.name` | string | The service name, from `OTEL_SERVICE_NAME` or defaults to `unknown_service`. |


Expand Down
3 changes: 2 additions & 1 deletion src/otlp/exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
use prost::Message;

use crate::config::{get_dash0_dataset, request_retries, request_timeout_ms};
use crate::otlp::log_mutations::{get_resources_attributes, map_logs_to_otlp};
use crate::otlp::log_mutations::map_logs_to_otlp;
use crate::otlp::resources::get_resources_attributes;
use crate::route::{ReqBody, HTTPS_CLIENT};
use crate::state::invocation_data::{
take_logs, take_metrics, StoredLog, StoredMetric, StoredTrace,
Expand Down
157 changes: 0 additions & 157 deletions src/otlp/log_mutations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,89 +309,6 @@ pub fn map_logs_to_otlp(logs: &[TelemetryLog]) -> Vec<LogRecord> {
log_records
}

pub fn try_read_env_from_file(key: &str) -> Option<String> {
let content = std::fs::read_to_string("/tmp/dash0_env_vars").ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
json.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}

pub fn get_resources_attributes() -> Vec<opentelemetry_proto::tonic::common::v1::KeyValue> {
use crate::otlp::attributes::*;
let mut attributes = vec![
opentelemetry_proto::tonic::common::v1::KeyValue {
key: CLOUD_PLATFORM.to_string(),
value: Some(AnyValue {
value: Some(
opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(
"aws_lambda".to_string(),
),
),
}),
},
opentelemetry_proto::tonic::common::v1::KeyValue {
key: CLOUD_RESOURCE_ID_SEMCONV.to_string(),
value: Some(AnyValue {
value: Some(
opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(
crate::state::global::get_function_arn()
.unwrap_or_else(|| "unknown".to_string()),
),
),
}),
},
opentelemetry_proto::tonic::common::v1::KeyValue {
key: CLOUD_ACCOUNT_ID.to_string(),
value: Some(AnyValue {
value: Some(
opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(
crate::state::global::get_account_id()
.unwrap_or_else(|| "unknown".to_string()),
),
),
}),
},
opentelemetry_proto::tonic::common::v1::KeyValue {
key: SERVICE_NAME.to_string(),
value: Some(AnyValue {
value: Some(
opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(
std::env::var("OTEL_SERVICE_NAME")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| try_read_env_from_file("OTEL_SERVICE_NAME"))
.unwrap_or_else(|| "unknown_service".to_string()),
),
),
}),
},
];

let resource_attributes = std::env::var("OTEL_RESOURCE_ATTRIBUTES")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| try_read_env_from_file("OTEL_RESOURCE_ATTRIBUTES"))
.unwrap_or_default();

for pair in resource_attributes.split(',') {
if let Some((key, value)) = pair.split_once('=') {
attributes.push(opentelemetry_proto::tonic::common::v1::KeyValue {
key: key.trim().to_string(),
value: Some(AnyValue {
value: Some(
opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(
value.trim().to_string(),
),
),
}),
});
}
}

attributes
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -549,80 +466,6 @@ mod tests {
);
}

#[test]
#[serial_test::serial]
fn test_get_resources_attributes_structure() {
let expected_service_name = "test-service-name";
std::env::set_var("OTEL_SERVICE_NAME", expected_service_name);

let attributes = get_resources_attributes();

let keys: Vec<String> = attributes.iter().map(|kv| kv.key.clone()).collect();
assert!(keys.contains(&"cloud.resource.id".to_string()));
assert!(keys.contains(&"cloud.account.id".to_string()));
assert!(keys.contains(&"service.name".to_string()));

let service_name_attr = attributes
.iter()
.find(|kv| kv.key == "service.name")
.unwrap();

assert_eq!(
get_string_value(&service_name_attr.value),
Some(expected_service_name.to_string())
);

// Cleanup
std::env::remove_var("OTEL_SERVICE_NAME");
}
#[test]
#[serial_test::serial]
fn test_get_resources_attributes_from_file_fallback() {
// Ensure env var is unset
std::env::remove_var("OTEL_SERVICE_NAME");
std::env::remove_var("OTEL_RESOURCE_ATTRIBUTES");

// Write mock file
let file_path = "/tmp/dash0_env_vars";
let expected_service_name = "service-from-file";
let expected_resource_attrs = "key1=value1,key2=value2";
let content = json!({
"OTEL_SERVICE_NAME": expected_service_name,
"OTEL_RESOURCE_ATTRIBUTES": expected_resource_attrs
})
.to_string();
std::fs::write(file_path, content).expect("Failed to write mock file");

let attributes = get_resources_attributes();

// Cleanup file
std::fs::remove_file(file_path).expect("Failed to cleanup mock file");

let service_name_attr = attributes
.iter()
.find(|kv| kv.key == "service.name")
.unwrap();

assert_eq!(
get_string_value(&service_name_attr.value),
Some(expected_service_name.to_string())
);

let key1_attr = attributes.iter().find(|kv| kv.key == "key1").unwrap();

assert_eq!(
get_string_value(&key1_attr.value),
Some("value1".to_string())
);

let key2_attr = attributes.iter().find(|kv| kv.key == "key2").unwrap();

assert_eq!(
get_string_value(&key2_attr.value),
Some("value2".to_string())
);
}

#[test]
fn test_map_logs_with_trace_and_span_id_from_store() {
use crate::state::invocation_entry;
Expand Down
70 changes: 20 additions & 50 deletions src/otlp/metrics_creation.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use crate::otlp::log_mutations::try_read_env_from_file;
use crate::otlp::resources::get_resources_attributes;
use crate::state::invocation_data::{store_metric, StoredMetric};
use crate::state::invocation_entry;
use hyper::header;
use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest;
use opentelemetry_proto::tonic::common::v1::any_value::Value;
use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue};
use opentelemetry_proto::tonic::common::v1::{InstrumentationScope, KeyValue};
use opentelemetry_proto::tonic::metrics::v1::metric::Data;
use opentelemetry_proto::tonic::metrics::v1::{
AggregationTemporality, Histogram, HistogramDataPoint, Metric, ResourceMetrics, ScopeMetrics,
Expand Down Expand Up @@ -93,29 +92,6 @@ fn create_histogram_metric(
}
}

fn get_metric_attributes() -> Vec<KeyValue> {
use crate::otlp::attributes::*;
vec![
KeyValue {
key: CLOUD_RESOURCE_ID.to_string(),
value: Some(AnyValue {
value: Some(Value::StringValue(
crate::state::global::get_function_arn()
.unwrap_or_else(|| "unknown".to_string()),
)),
}),
},
KeyValue {
key: CLOUD_ACCOUNT_ID.to_string(),
value: Some(AnyValue {
value: Some(Value::StringValue(
crate::state::global::get_account_id().unwrap_or_else(|| "unknown".to_string()),
)),
}),
},
]
}

pub fn create_metrics(invocation_id: &str) -> Option<StoredMetric> {
let data = invocation_entry::get_metrics_data(invocation_id)?;

Expand All @@ -134,7 +110,7 @@ pub fn create_metrics(invocation_id: &str) -> Option<StoredMetric> {
"Duration of the invocation",
"s",
data.duration / 1000.0,
get_metric_attributes(),
Vec::new(),
start_time_unix_nano,
time_unix_nano,
DURATION_BOUNDS,
Expand All @@ -147,7 +123,7 @@ pub fn create_metrics(invocation_id: &str) -> Option<StoredMetric> {
"Duration of the cold start initialization",
"s",
data.init_duration / 1000.0,
get_metric_attributes(),
Vec::new(),
start_time_unix_nano,
time_unix_nano,
DURATION_BOUNDS,
Expand All @@ -160,7 +136,7 @@ pub fn create_metrics(invocation_id: &str) -> Option<StoredMetric> {
"Billed duration of the invocation",
"s",
data.billed_duration / 1000.0,
get_metric_attributes(),
Vec::new(),
start_time_unix_nano,
time_unix_nano,
DURATION_BOUNDS,
Expand All @@ -173,7 +149,7 @@ pub fn create_metrics(invocation_id: &str) -> Option<StoredMetric> {
"Memory used by the invocation",
"By",
data.memory_usage as f64 * 1024.0 * 1024.0,
get_metric_attributes(),
Vec::new(),
start_time_unix_nano,
time_unix_nano,
MEMORY_BOUNDS,
Expand All @@ -197,18 +173,7 @@ pub fn create_metrics(invocation_id: &str) -> Option<StoredMetric> {
};

let resource = Resource {
attributes: vec![KeyValue {
key: crate::otlp::attributes::SERVICE_NAME.to_string(),
value: Some(AnyValue {
value: Some(Value::StringValue(
std::env::var("OTEL_SERVICE_NAME")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| try_read_env_from_file("OTEL_SERVICE_NAME"))
.unwrap_or_else(|| "unknown_service".to_string()),
)),
}),
}],
attributes: get_resources_attributes(),
..Default::default()
};

Expand Down Expand Up @@ -314,13 +279,8 @@ mod tests {
assert_eq!(dp.start_time_unix_nano, 850_000_000); // (1000 - 150) * 1_000_000
assert_eq!(dp.time_unix_nano, 1_200_000_000); // 1200 * 1_000_000

// Check attributes — no high-cardinality invocation_id on metrics
assert!(!dp
.attributes
.iter()
.any(|kv| kv.key == "faas.invocation_id"));
assert!(dp.attributes.iter().any(|kv| kv.key == "cloud.resource_id"));
assert!(dp.attributes.iter().any(|kv| kv.key == "cloud.account.id"));
// Data points carry no attributes — cloud.* live on the resource.
assert!(dp.attributes.is_empty());
} else {
panic!("Expected Histogram data for faas.invoke_duration");
}
Expand Down Expand Up @@ -353,8 +313,18 @@ mod tests {
panic!("Expected Histogram data for faas.mem_usage");
}

// Check resource service.name
// Check resource attributes — aligned with logs/spans.
let resource = decoded.resource_metrics[0].resource.as_ref().unwrap();
let resource_keys: Vec<&str> = resource
.attributes
.iter()
.map(|kv| kv.key.as_str())
.collect();
assert!(resource_keys.contains(&"service.name"));
assert!(resource_keys.contains(&"cloud.platform"));
assert!(resource_keys.contains(&"cloud.resource.id"));
assert!(resource_keys.contains(&"cloud.account.id"));

let service_name = resource
.attributes
.iter()
Expand Down
1 change: 1 addition & 0 deletions src/otlp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod masking;
pub mod metrics_creation;
pub mod metrics_receiver;
pub mod receiver;
pub mod resources;
pub mod span_creation;
pub mod span_link_extractor;
pub mod span_mutations;
Expand Down
Loading
Loading