Skip to main content

awssdk_instrumentation/lambda/layer/
utils.rs

1//! Utilities for parsing the X-Ray trace header and defining the `faas.trigger` attribute.
2
3use opentelemetry::{SpanId, TraceFlags, TraceId};
4
5/// Parsed X-Ray trace header containing the trace ID, parent span ID, and sampling flag.
6#[derive(Debug)]
7pub(super) struct XRayTraceHeader {
8    pub(super) trace_id: TraceId,
9    pub(super) parent_id: SpanId,
10    pub(super) sampled: TraceFlags,
11}
12impl XRayTraceHeader {
13    /// `Root` field key in the X-Ray trace header.
14    const ROOT: &str = "Root";
15    /// `Parent` field key in the X-Ray trace header.
16    const PARENT: &str = "Parent";
17    /// `Sampled` field key in the X-Ray trace header.
18    const SAMPLE: &str = "Sampled";
19    /// `Lineage` field key in the X-Ray trace header (ignored during parsing).
20    const LINEAGE: &str = "Lineage";
21    /// Delimiter between key-value pairs in the X-Ray trace header.
22    const HEADER_DELIMITER: &str = ";";
23}
24/// Parses an X-Ray trace header string (e.g. from `_X_AMZN_TRACE_ID`) into an [`XRayTraceHeader`].
25impl core::str::FromStr for XRayTraceHeader {
26    type Err = String;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        let mut xray_header = Self {
30            trace_id: TraceId::INVALID,
31            parent_id: SpanId::INVALID,
32            sampled: TraceFlags::SAMPLED,
33        };
34        let mut trace_id_collected = false;
35        let mut parent_id_collected = false;
36        let mut sampled_collected = false;
37
38        fn map_err(e: impl ToString) -> String {
39            e.to_string()
40        }
41        for (key, value) in s
42            .split(Self::HEADER_DELIMITER)
43            .filter_map(|part| part.split_once('='))
44        {
45            match key {
46                Self::ROOT if !trace_id_collected => {
47                    xray_header.trace_id =
48                        TraceId::from_hex(&value.split('-').skip(1).collect::<String>())
49                            .map_err(map_err)?;
50                    trace_id_collected = true;
51                }
52                Self::PARENT if !parent_id_collected => {
53                    xray_header.parent_id = SpanId::from_hex(value).map_err(map_err)?;
54                    parent_id_collected = true;
55                }
56                Self::SAMPLE if !sampled_collected => {
57                    xray_header.sampled = match value {
58                        "0" => TraceFlags::NOT_SAMPLED,
59                        "1" => TraceFlags::SAMPLED,
60                        _ => return Err("Invalid Trace header".to_owned()),
61                    };
62                    sampled_collected = true;
63                }
64                Self::LINEAGE => {
65                    // Ignored
66                }
67                // Ignore unrecognized keys — the X-Ray header format may be extended
68                // with new fields in the future
69                _ => {}
70            }
71        }
72
73        if !(trace_id_collected && parent_id_collected && sampled_collected) {
74            return Err("Invalid Trace header".to_owned());
75        }
76
77        Ok(xray_header)
78    }
79}
80
81/// The value of the OpenTelemetry `faas.trigger` attribute for a Lambda invocation.
82///
83/// Pass a variant to [`TracingLayer::with_trigger`] to describe what kind of
84/// event triggers your Lambda function. The value is set on the per-invocation
85/// span as the `faas.trigger` attribute.
86///
87/// The default variant is [`Datasource`], which is appropriate for Lambda
88/// functions that read from or write to a data store such as DynamoDB or S3.
89///
90/// See the [OTel FaaS attributes registry](https://opentelemetry.io/docs/specs/semconv/attributes-registry/faas/)
91/// for the full specification.
92///
93/// # Examples
94///
95/// ```
96/// use awssdk_instrumentation::lambda::OTelFaasTrigger;
97///
98/// assert_eq!(OTelFaasTrigger::Datasource.to_string(), "datasource");
99/// assert_eq!(OTelFaasTrigger::Http.to_string(), "http");
100/// assert_eq!(OTelFaasTrigger::PubSub.to_string(), "pubsub");
101/// assert_eq!(OTelFaasTrigger::Timer.to_string(), "timer");
102/// assert_eq!(OTelFaasTrigger::Other.to_string(), "other");
103/// ```
104///
105/// [`TracingLayer::with_trigger`]: crate::lambda::layer::TracingLayer::with_trigger
106/// [`Datasource`]: OTelFaasTrigger::Datasource
107#[derive(Debug, Default, Clone, Copy)]
108#[non_exhaustive]
109pub enum OTelFaasTrigger {
110    /// A response to a data source operation such as a database or filesystem read/write.
111    ///
112    /// This is the default. Use it for Lambda functions triggered by DynamoDB
113    /// Streams, S3 events, or other data-store events.
114    #[default]
115    Datasource,
116    /// A response to an inbound HTTP request.
117    ///
118    /// Use this for Lambda functions fronted by API Gateway or a Function URL.
119    Http,
120    /// A function invoked when messages are sent to a messaging system.
121    ///
122    /// Use this for Lambda functions triggered by SQS, SNS, or EventBridge.
123    PubSub,
124    /// A function scheduled to run at regular intervals.
125    ///
126    /// Use this for Lambda functions triggered by EventBridge Scheduler or
127    /// CloudWatch Events rules.
128    Timer,
129    /// None of the other trigger types apply.
130    Other,
131}
132
133/// Formats the trigger as the lowercase string value used in the `faas.trigger`
134/// OTel attribute (e.g. `"datasource"`, `"http"`).
135impl std::fmt::Display for OTelFaasTrigger {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            OTelFaasTrigger::Datasource => write!(f, "datasource"),
139            OTelFaasTrigger::Http => write!(f, "http"),
140            OTelFaasTrigger::PubSub => write!(f, "pubsub"),
141            OTelFaasTrigger::Timer => write!(f, "timer"),
142            OTelFaasTrigger::Other => write!(f, "other"),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use opentelemetry::{SpanId, TraceFlags, TraceId};
151
152    // Tests for XRayTraceHeader::from_str — comprehensive
153
154    #[test]
155    fn xray_trace_header_valid_sampled_1() {
156        let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
157        let parsed: XRayTraceHeader = header.parse().unwrap();
158
159        assert_eq!(
160            parsed.trace_id,
161            TraceId::from_hex("5759e988bd862e3fe1be46a994272793").unwrap()
162        );
163        assert_eq!(
164            parsed.parent_id,
165            SpanId::from_hex("53995c3f42cd8ad8").unwrap()
166        );
167        assert_eq!(parsed.sampled, TraceFlags::SAMPLED);
168    }
169
170    #[test]
171    fn xray_trace_header_valid_sampled_0() {
172        let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=0";
173        let parsed: XRayTraceHeader = header.parse().unwrap();
174
175        assert_eq!(
176            parsed.trace_id,
177            TraceId::from_hex("5759e988bd862e3fe1be46a994272793").unwrap()
178        );
179        assert_eq!(
180            parsed.parent_id,
181            SpanId::from_hex("53995c3f42cd8ad8").unwrap()
182        );
183        assert_eq!(parsed.sampled, TraceFlags::NOT_SAMPLED);
184    }
185
186    #[test]
187    fn xray_trace_header_valid_with_lineage_field() {
188        let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1;Lineage=a87bd80c:1|68fd508a:5|c512fbe3:2";
189        let parsed: XRayTraceHeader = header.parse().unwrap();
190
191        assert_eq!(
192            parsed.trace_id,
193            TraceId::from_hex("5759e988bd862e3fe1be46a994272793").unwrap()
194        );
195        assert_eq!(
196            parsed.parent_id,
197            SpanId::from_hex("53995c3f42cd8ad8").unwrap()
198        );
199        assert_eq!(parsed.sampled, TraceFlags::SAMPLED);
200    }
201
202    #[test]
203    fn xray_trace_header_valid_with_unknown_fields() {
204        let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1;FutureField=somevalue";
205        let parsed: XRayTraceHeader = header.parse().unwrap();
206
207        assert_eq!(
208            parsed.trace_id,
209            TraceId::from_hex("5759e988bd862e3fe1be46a994272793").unwrap()
210        );
211        assert_eq!(
212            parsed.parent_id,
213            SpanId::from_hex("53995c3f42cd8ad8").unwrap()
214        );
215        assert_eq!(parsed.sampled, TraceFlags::SAMPLED);
216    }
217
218    #[test]
219    fn xray_trace_header_missing_parent_field() {
220        let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1";
221        let result: Result<XRayTraceHeader, _> = header.parse();
222        assert!(result.is_err());
223    }
224
225    #[test]
226    fn xray_trace_header_invalid_sampled_value() {
227        let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=2";
228        let result: Result<XRayTraceHeader, _> = header.parse();
229        assert!(result.is_err());
230    }
231
232    #[test]
233    fn xray_trace_header_empty_string() {
234        let result: Result<XRayTraceHeader, _> = "".parse();
235        assert!(result.is_err());
236    }
237}