diff --git a/CHANGELOG.md b/CHANGELOG.md index c100a3d23c..6d140f91b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Features**: - Dynamic sampling is now based on the volume received by Relay by default and does not include the original volume dropped by client-side sampling in SDKs. This is required for the final dynamic sampling feature in the latest Sentry plans. ([#1591](https://github.com/getsentry/relay/pull/1591)) +- Add OpenTelemetry Context ([#1617](https://github.com/getsentry/relay/pull/1617)) **Internal**: diff --git a/relay-general/src/protocol/contexts/mod.rs b/relay-general/src/protocol/contexts/mod.rs index 32b40dcc10..6ada0ccd1b 100644 --- a/relay-general/src/protocol/contexts/mod.rs +++ b/relay-general/src/protocol/contexts/mod.rs @@ -18,6 +18,8 @@ mod runtime; pub use runtime::*; mod trace; pub use trace::*; +mod otel; +pub use otel::*; use crate::types::{Annotated, FromValue, Object, Value}; @@ -51,6 +53,8 @@ pub enum Context { Reprocessing(Box), /// Response information. Response(Box), + /// OpenTelemetry information + Otel(Box), /// Additional arbitrary fields for forwards compatibility. #[metastructure(fallback_variant)] Other(#[metastructure(pii = "true")] Object), @@ -72,6 +76,7 @@ impl Context { Context::Trace(_) => Some(TraceContext::default_key()), Context::Monitor(_) => Some(MonitorContext::default_key()), Context::Response(_) => Some(ResponseContext::default_key()), + Context::Otel(_) => Some(OtelContext::default_key()), Context::Other(_) => None, } } diff --git a/relay-general/src/protocol/contexts/otel.rs b/relay-general/src/protocol/contexts/otel.rs new file mode 100644 index 0000000000..3de750ec25 --- /dev/null +++ b/relay-general/src/protocol/contexts/otel.rs @@ -0,0 +1,146 @@ +use crate::types::{Annotated, Object, Value}; + +/// OpenTelemetry Context +/// +/// If an event has this context, it was generated from an OpenTelemetry signal (trace, metric, log). +#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] +pub struct OtelContext { + /// Attributes of the OpenTelemetry span that maps to a Sentry event. + /// + /// + #[metastructure(pii = "maybe", bag_size = "large")] + attributes: Annotated>, + + /// Information about an OpenTelemetry resource. + /// + /// + #[metastructure(pii = "maybe", bag_size = "large")] + resource: Annotated>, + + /// Additional arbitrary fields for forwards compatibility. + #[metastructure(additional_properties, retain = "true", pii = "maybe")] + pub other: Object, +} + +impl OtelContext { + /// The key under which a runtime context is generally stored (in `Contexts`). + pub fn default_key() -> &'static str { + "otel" + } +} + +#[cfg(test)] +mod tests { + use crate::protocol::Context; + + use super::*; + + #[test] + pub(crate) fn test_otel_context_roundtrip() { + let json = r#"{ + "attributes": { + "app.payment.amount": 394.25, + "rpc.grpc.status_code": "1", + "rpc.method": "Charge", + "rpc.service": "hipstershop.PaymentService", + "rpc.system": "grpc" + }, + "resource": { + "process.command": "/usr/src/app/index.js", + "process.command_line": "/usr/local/bin/node /usr/src/app/index.js", + "process.executable.name": "node", + "process.pid": 1, + "process.runtime.description": "Node.js", + "process.runtime.name": "nodejs", + "process.runtime.version": "16.18.0", + "service.name": "paymentservice", + "telemetry.sdk.language": "nodejs", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.7.0" + }, + "other": "value", + "type": "otel" +}"#; + let context = Annotated::new(Context::Otel(Box::new(OtelContext { + attributes: Annotated::new(Object::from([ + ( + "app.payment.amount".to_string(), + Annotated::new(Value::F64(394.25)), + ), + ( + "rpc.grpc.status_code".to_string(), + Annotated::new(Value::String("1".to_string())), + ), + ( + "rpc.method".to_string(), + Annotated::new(Value::String("Charge".to_string())), + ), + ( + "rpc.service".to_string(), + Annotated::new(Value::String("hipstershop.PaymentService".to_string())), + ), + ( + "rpc.system".to_string(), + Annotated::new(Value::String("grpc".to_string())), + ), + ])), + resource: Annotated::new(Object::from([ + ( + "process.command".to_string(), + Annotated::new(Value::String("/usr/src/app/index.js".to_string())), + ), + ( + "process.command_line".to_string(), + Annotated::new(Value::String( + "/usr/local/bin/node /usr/src/app/index.js".to_string(), + )), + ), + ( + "process.executable.name".to_string(), + Annotated::new(Value::String("node".to_string())), + ), + ("process.pid".to_string(), Annotated::new(Value::I64(1))), + ( + "process.runtime.description".to_string(), + Annotated::new(Value::String("Node.js".to_string())), + ), + ( + "process.runtime.name".to_string(), + Annotated::new(Value::String("nodejs".to_string())), + ), + ( + "process.runtime.version".to_string(), + Annotated::new(Value::String("16.18.0".to_string())), + ), + ( + "service.name".to_string(), + Annotated::new(Value::String("paymentservice".to_string())), + ), + ( + "telemetry.sdk.language".to_string(), + Annotated::new(Value::String("nodejs".to_string())), + ), + ( + "telemetry.sdk.name".to_string(), + Annotated::new(Value::String("opentelemetry".to_string())), + ), + ( + "telemetry.sdk.version".to_string(), + Annotated::new(Value::String("1.7.0".to_string())), + ), + ])), + other: { + let mut map = Object::new(); + map.insert( + "other".to_string(), + Annotated::new(Value::String("value".to_string())), + ); + map + }, + }))); + + assert_eq!(context, Annotated::from_json(json).unwrap()); + assert_eq!(json, context.to_json_pretty().unwrap()); + } +} diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap b/relay-general/tests/snapshots/test_fixtures__event_schema.snap index 7f5a54637a..2d1c6f7e6e 100644 --- a/relay-general/tests/snapshots/test_fixtures__event_schema.snap +++ b/relay-general/tests/snapshots/test_fixtures__event_schema.snap @@ -1,6 +1,5 @@ --- source: relay-general/tests/test_fixtures.rs -assertion_line: 109 expression: "relay_general::protocol::event_json_schema()" --- { @@ -825,6 +824,9 @@ expression: "relay_general::protocol::event_json_schema()" { "$ref": "#/definitions/ResponseContext" }, + { + "$ref": "#/definitions/OtelContext" + }, { "type": "object", "additionalProperties": true @@ -2368,6 +2370,35 @@ expression: "relay_general::protocol::event_json_schema()" } ] }, + "OtelContext": { + "description": " OpenTelemetry Context\n\n If an event has this context, it was generated from an OpenTelemetry signal (trace, metric, log).", + "anyOf": [ + { + "type": "object", + "properties": { + "attributes": { + "description": " Attributes of the OpenTelemetry span that maps to a Sentry event.\n\n ", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "resource": { + "description": " Information about an OpenTelemetry resource.\n\n ", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "additionalProperties": false + } + ] + }, "PosixSignal": { "description": " POSIX signal with optional extended data.\n\n On Apple systems, signals also carry a code in addition to the signal number describing the\n signal in more detail. On Linux, this code does not exist.", "anyOf": [