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}