Skip to main content

aws_smithy_types/
telemetry.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Types for carrying values selected from an operation's input through to telemetry.
7//!
8//! Telemetry runs at the generic layer, where the operation input is type-erased and, during
9//! serialization, consumed before any result-bearing hook runs. After that point a value such as
10//! the target resource identifier survives only inside the serialized request. `CapturedTelemetryAttributes`
11//! is the bridge: generated code selects an input member once, before the input is consumed, and
12//! writes it here into the `ConfigBag`. Any downstream interceptor — and the built-in metrics
13//! implementation — can then read it via `cfg.load`.
14//!
15//! This type lives in `aws-smithy-types` (a stable crate) deliberately: it carries no dependency on
16//! `aws-smithy-observability` and can therefore appear in stable, generated configuration without
17//! leaking a 0.x type.
18//!
19//! It is off by default. When no input member is selected, nothing is captured and this value is
20//! absent from the `ConfigBag`.
21
22use crate::config_bag::{Storable, StoreReplace};
23use std::collections::HashMap;
24use std::sync::Arc;
25
26/// A set of string-keyed values selected from an operation's input, carried through the `ConfigBag`
27/// for telemetry.
28///
29/// Cheap to clone regardless of value length, so propagating it as the `ConfigBag` moves through
30/// config-bag layers stays inexpensive.
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
32pub struct CapturedTelemetryAttributes {
33    values: HashMap<Arc<str>, Arc<str>>,
34}
35
36impl CapturedTelemetryAttributes {
37    /// Creates an empty set.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Inserts a captured value under `name`, replacing any existing value for that name.
43    ///
44    /// Takes `impl AsRef<str>` so the public API doesn't commit to the internal storage type;
45    /// values are cloned into the backing representation here.
46    pub fn insert(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) {
47        self.values
48            .insert(Arc::from(name.as_ref()), Arc::from(value.as_ref()));
49    }
50
51    /// Returns the captured value for `name`, if one was captured.
52    ///
53    /// This is the read path for a downstream interceptor that wants a captured value directly,
54    /// e.g. `cfg.load::<CapturedTelemetryAttributes>().and_then(|a| a.get("Bucket"))`.
55    pub fn get(&self, name: &str) -> Option<&str> {
56        self.values.get(name).map(|v| v.as_ref())
57    }
58
59    /// Iterates over the captured `(name, value)` pairs.
60    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
61        self.values.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
62    }
63}
64
65impl Storable for CapturedTelemetryAttributes {
66    type Storer = StoreReplace<Self>;
67}
68
69/// The operation-input member names a customer has opted in to for telemetry, split into two
70/// independent policies.
71///
72/// Every requested member is *captured* into `CapturedTelemetryAttributes` on the config bag; the
73/// two sets differ only in whether the value is also *emitted* on the built-in client metrics:
74/// * **emit** — the value is captured *and* attached to the built-in client metrics as an
75///   attribute. This is the common case (`emit_input_attributes`).
76/// * **capture-only** — the value is captured so a custom interceptor can read it during the
77///   operation, but it is *not* attached to the built-in metrics (`capture_input_attributes`).
78///   This keeps a high-cardinality value out of the metric label set while still making it
79///   available in-process.
80///
81/// The generated per-operation interceptor captures the *union* of both sets; the built-in metrics
82/// implementation emits only the *emit* set. Absent unless the customer opts in, so both are a
83/// no-op by default.
84///
85/// Names are the Smithy member names (e.g. `"Bucket"`), matched by generated code against the
86/// operation's input members.
87#[derive(Clone, Debug, Default, PartialEq, Eq)]
88pub struct RequestedTelemetryAttributes {
89    emit: Vec<Arc<str>>,
90    capture_only: Vec<Arc<str>>,
91}
92
93impl RequestedTelemetryAttributes {
94    /// Creates a selection whose members are both captured and emitted on the metrics.
95    ///
96    /// Takes `impl AsRef<str>` items so the public API doesn't commit to the internal storage
97    /// type; names are cloned into the backing representation here.
98    pub fn new(names: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
99        Self {
100            emit: names.into_iter().map(|n| Arc::from(n.as_ref())).collect(),
101            capture_only: Vec::new(),
102        }
103    }
104
105    /// Adds member names to the *emit* set (captured and attached to the built-in metrics).
106    pub fn emit(&mut self, names: impl IntoIterator<Item = impl AsRef<str>>) {
107        self.emit
108            .extend(names.into_iter().map(|n| Arc::from(n.as_ref())));
109    }
110
111    /// Adds member names to the *capture-only* set (captured for in-process reads, not emitted on
112    /// the built-in metrics).
113    pub fn capture_only(&mut self, names: impl IntoIterator<Item = impl AsRef<str>>) {
114        self.capture_only
115            .extend(names.into_iter().map(|n| Arc::from(n.as_ref())));
116    }
117
118    /// Returns `true` if `name` should be captured (in either set).
119    pub fn should_capture(&self, name: &str) -> bool {
120        self.emit
121            .iter()
122            .chain(self.capture_only.iter())
123            .any(|n| n.as_ref() == name)
124    }
125
126    /// Returns `true` if `name` should be emitted on the built-in metrics.
127    pub fn should_emit(&self, name: &str) -> bool {
128        self.emit.iter().any(|n| n.as_ref() == name)
129    }
130
131    /// Returns `true` if nothing is requested for capture in either set.
132    pub fn is_empty(&self) -> bool {
133        self.emit.is_empty() && self.capture_only.is_empty()
134    }
135}
136
137impl Storable for RequestedTelemetryAttributes {
138    type Storer = StoreReplace<Self>;
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn insert_and_get() {
147        let mut attrs = CapturedTelemetryAttributes::new();
148        assert_eq!(attrs.iter().count(), 0);
149
150        attrs.insert("bucket", "example-bucket");
151        assert_eq!(attrs.get("bucket"), Some("example-bucket"));
152        assert_eq!(attrs.get("missing"), None);
153        assert_eq!(attrs.iter().count(), 1);
154    }
155
156    #[test]
157    fn insert_replaces_existing() {
158        let mut attrs = CapturedTelemetryAttributes::new();
159        attrs.insert("bucket", "first");
160        attrs.insert("bucket", "second");
161        assert_eq!(attrs.get("bucket"), Some("second"));
162        assert_eq!(attrs.iter().count(), 1);
163    }
164
165    #[test]
166    fn iter_yields_all_pairs() {
167        let mut attrs = CapturedTelemetryAttributes::new();
168        attrs.insert("bucket", "b");
169        attrs.insert("table", "t");
170        let mut pairs: Vec<_> = attrs.iter().collect();
171        pairs.sort();
172        assert_eq!(pairs, vec![("bucket", "b"), ("table", "t")]);
173    }
174
175    #[test]
176    fn emit_set_is_captured_and_emitted() {
177        let requested = RequestedTelemetryAttributes::new(["Bucket", "Key"]);
178        // Members in the emit set are both captured and emitted.
179        assert!(requested.should_capture("Bucket"));
180        assert!(requested.should_emit("Bucket"));
181        assert!(requested.should_capture("Key"));
182        assert!(requested.should_emit("Key"));
183        assert!(!requested.should_capture("VersionId"));
184        assert!(!requested.is_empty());
185
186        let empty = RequestedTelemetryAttributes::default();
187        assert!(empty.is_empty());
188        assert!(!empty.should_capture("Bucket"));
189    }
190
191    #[test]
192    fn capture_only_set_is_captured_but_not_emitted() {
193        let mut requested = RequestedTelemetryAttributes::default();
194        requested.emit(["Bucket"]);
195        requested.capture_only(["Prefix"]);
196
197        // Prefix is captured for in-process reads but must not be emitted on the metrics.
198        assert!(requested.should_capture("Prefix"));
199        assert!(!requested.should_emit("Prefix"));
200
201        // Bucket stays both captured and emitted.
202        assert!(requested.should_capture("Bucket"));
203        assert!(requested.should_emit("Bucket"));
204
205        assert!(!requested.is_empty());
206    }
207}