@temporalio/core-bridge 1.12.0 → 1.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/Cargo.lock +64 -119
  2. package/Cargo.toml +1 -1
  3. package/index.js +3 -2
  4. package/package.json +3 -3
  5. package/releases/aarch64-apple-darwin/index.node +0 -0
  6. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
  7. package/releases/x86_64-apple-darwin/index.node +0 -0
  8. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  9. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  10. package/sdk-core/.cargo/config.toml +1 -2
  11. package/sdk-core/.github/workflows/per-pr.yml +2 -0
  12. package/sdk-core/AGENTS.md +7 -0
  13. package/sdk-core/Cargo.toml +9 -5
  14. package/sdk-core/README.md +6 -5
  15. package/sdk-core/client/Cargo.toml +3 -2
  16. package/sdk-core/client/src/lib.rs +17 -8
  17. package/sdk-core/client/src/metrics.rs +57 -23
  18. package/sdk-core/client/src/raw.rs +33 -15
  19. package/sdk-core/core/Cargo.toml +11 -9
  20. package/sdk-core/core/benches/workflow_replay.rs +114 -15
  21. package/sdk-core/core/src/core_tests/activity_tasks.rs +18 -18
  22. package/sdk-core/core/src/core_tests/child_workflows.rs +4 -4
  23. package/sdk-core/core/src/core_tests/determinism.rs +6 -6
  24. package/sdk-core/core/src/core_tests/local_activities.rs +20 -20
  25. package/sdk-core/core/src/core_tests/mod.rs +40 -5
  26. package/sdk-core/core/src/core_tests/queries.rs +25 -16
  27. package/sdk-core/core/src/core_tests/replay_flag.rs +3 -3
  28. package/sdk-core/core/src/core_tests/updates.rs +3 -3
  29. package/sdk-core/core/src/core_tests/workers.rs +9 -7
  30. package/sdk-core/core/src/core_tests/workflow_tasks.rs +40 -42
  31. package/sdk-core/core/src/ephemeral_server/mod.rs +1 -19
  32. package/sdk-core/core/src/lib.rs +10 -1
  33. package/sdk-core/core/src/pollers/poll_buffer.rs +2 -2
  34. package/sdk-core/core/src/replay/mod.rs +3 -3
  35. package/sdk-core/core/src/telemetry/metrics.rs +306 -152
  36. package/sdk-core/core/src/telemetry/mod.rs +11 -4
  37. package/sdk-core/core/src/telemetry/otel.rs +134 -131
  38. package/sdk-core/core/src/telemetry/prometheus_meter.rs +885 -0
  39. package/sdk-core/core/src/telemetry/prometheus_server.rs +48 -28
  40. package/sdk-core/core/src/test_help/mod.rs +27 -12
  41. package/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +7 -7
  42. package/sdk-core/core/src/worker/activities.rs +4 -4
  43. package/sdk-core/core/src/worker/client/mocks.rs +10 -3
  44. package/sdk-core/core/src/worker/client.rs +68 -5
  45. package/sdk-core/core/src/worker/heartbeat.rs +229 -0
  46. package/sdk-core/core/src/worker/mod.rs +35 -14
  47. package/sdk-core/core/src/worker/tuner/resource_based.rs +4 -4
  48. package/sdk-core/core/src/worker/workflow/history_update.rs +71 -19
  49. package/sdk-core/core/src/worker/workflow/machines/cancel_external_state_machine.rs +1 -2
  50. package/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +1 -1
  51. package/sdk-core/core/src/worker/workflow/machines/nexus_operation_state_machine.rs +31 -48
  52. package/sdk-core/core/src/worker/workflow/machines/signal_external_state_machine.rs +1 -2
  53. package/sdk-core/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs +3 -3
  54. package/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +4 -1
  55. package/sdk-core/core/src/worker/workflow/managed_run.rs +1 -1
  56. package/sdk-core/core/src/worker/workflow/mod.rs +15 -15
  57. package/sdk-core/core-api/Cargo.toml +2 -2
  58. package/sdk-core/core-api/src/envconfig.rs +204 -99
  59. package/sdk-core/core-api/src/lib.rs +9 -0
  60. package/sdk-core/core-api/src/telemetry/metrics.rs +548 -100
  61. package/sdk-core/core-api/src/worker.rs +11 -5
  62. package/sdk-core/core-c-bridge/Cargo.toml +49 -0
  63. package/sdk-core/core-c-bridge/build.rs +26 -0
  64. package/sdk-core/core-c-bridge/include/temporal-sdk-core-c-bridge.h +817 -0
  65. package/sdk-core/core-c-bridge/src/client.rs +679 -0
  66. package/sdk-core/core-c-bridge/src/lib.rs +245 -0
  67. package/sdk-core/core-c-bridge/src/metric.rs +682 -0
  68. package/sdk-core/core-c-bridge/src/random.rs +61 -0
  69. package/sdk-core/core-c-bridge/src/runtime.rs +445 -0
  70. package/sdk-core/core-c-bridge/src/testing.rs +282 -0
  71. package/sdk-core/core-c-bridge/src/tests/context.rs +644 -0
  72. package/sdk-core/core-c-bridge/src/tests/mod.rs +178 -0
  73. package/sdk-core/core-c-bridge/src/tests/utils.rs +108 -0
  74. package/sdk-core/core-c-bridge/src/worker.rs +1069 -0
  75. package/sdk-core/etc/deps.svg +64 -64
  76. package/sdk-core/sdk/src/activity_context.rs +6 -4
  77. package/sdk-core/sdk/src/lib.rs +49 -27
  78. package/sdk-core/sdk/src/workflow_future.rs +18 -25
  79. package/sdk-core/sdk-core-protos/protos/api_upstream/README.md +4 -0
  80. package/sdk-core/sdk-core-protos/protos/api_upstream/buf.yaml +0 -2
  81. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv2.json +630 -83
  82. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv3.yaml +632 -78
  83. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/batch/v1/message.proto +4 -4
  84. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/command/v1/message.proto +6 -4
  85. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/common/v1/message.proto +2 -2
  86. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/deployment/v1/message.proto +32 -2
  87. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/common.proto +10 -1
  88. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/deployment.proto +26 -0
  89. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/failed_cause.proto +2 -0
  90. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/reset.proto +4 -4
  91. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/failure/v1/message.proto +2 -2
  92. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/history/v1/message.proto +47 -31
  93. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/nexus/v1/message.proto +4 -4
  94. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/schedule/v1/message.proto +7 -1
  95. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/worker/v1/message.proto +134 -0
  96. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflow/v1/message.proto +14 -11
  97. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +148 -37
  98. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +21 -0
  99. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +4 -4
  100. package/sdk-core/sdk-core-protos/src/history_builder.rs +9 -5
  101. package/sdk-core/sdk-core-protos/src/lib.rs +96 -6
  102. package/sdk-core/test-utils/src/lib.rs +11 -3
  103. package/sdk-core/tests/cloud_tests.rs +3 -3
  104. package/sdk-core/tests/heavy_tests.rs +11 -3
  105. package/sdk-core/tests/integ_tests/client_tests.rs +12 -13
  106. package/sdk-core/tests/integ_tests/ephemeral_server_tests.rs +1 -1
  107. package/sdk-core/tests/integ_tests/metrics_tests.rs +188 -83
  108. package/sdk-core/tests/integ_tests/polling_tests.rs +1 -1
  109. package/sdk-core/tests/integ_tests/queries_tests.rs +56 -40
  110. package/sdk-core/tests/integ_tests/update_tests.rs +2 -7
  111. package/sdk-core/tests/integ_tests/worker_tests.rs +3 -4
  112. package/sdk-core/tests/integ_tests/worker_versioning_tests.rs +3 -7
  113. package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +3 -5
  114. package/sdk-core/tests/integ_tests/workflow_tests/nexus.rs +24 -17
  115. package/src/client.rs +6 -0
  116. package/src/metrics.rs +6 -6
@@ -0,0 +1,885 @@
1
+ use crate::{abstractions::dbg_panic, telemetry::default_buckets_for};
2
+ use anyhow::anyhow;
3
+ use parking_lot::RwLock;
4
+ use prometheus::{
5
+ GaugeVec, HistogramVec, IntCounterVec, IntGaugeVec, Opts,
6
+ core::{Collector, Desc, GenericCounter},
7
+ proto::{LabelPair, MetricFamily},
8
+ };
9
+ use std::{
10
+ collections::{BTreeMap, HashMap, HashSet, btree_map, hash_map},
11
+ fmt::{Debug, Formatter},
12
+ sync::Arc,
13
+ time::Duration,
14
+ };
15
+ use temporal_sdk_core_api::telemetry::metrics::{
16
+ CoreMeter, Counter, CounterBase, Gauge, GaugeBase, GaugeF64, GaugeF64Base, Histogram,
17
+ HistogramBase, HistogramDuration, HistogramDurationBase, HistogramF64, HistogramF64Base,
18
+ MetricAttributable, MetricAttributes, MetricParameters, NewAttributes, OrderedPromLabelSet,
19
+ };
20
+
21
+ #[derive(derive_more::From, derive_more::TryInto, Debug, Clone)]
22
+ enum PromCollector {
23
+ Histo(HistogramVec),
24
+ Counter(IntCounterVec),
25
+ Gauge(IntGaugeVec),
26
+ GaugeF64(GaugeVec),
27
+ }
28
+
29
+ impl Collector for PromCollector {
30
+ fn desc(&self) -> Vec<&Desc> {
31
+ match self {
32
+ PromCollector::Histo(v) => v.desc(),
33
+ PromCollector::Counter(v) => v.desc(),
34
+ PromCollector::Gauge(v) => v.desc(),
35
+ PromCollector::GaugeF64(v) => v.desc(),
36
+ }
37
+ }
38
+
39
+ fn collect(&self) -> Vec<MetricFamily> {
40
+ match self {
41
+ PromCollector::Histo(v) => v.collect(),
42
+ PromCollector::Counter(v) => v.collect(),
43
+ PromCollector::Gauge(v) => v.collect(),
44
+ PromCollector::GaugeF64(v) => v.collect(),
45
+ }
46
+ }
47
+ }
48
+
49
+ /// Replaces Prometheus's default registry with a custom one that allows us to register metrics that
50
+ /// have different label sets for the same name.
51
+ #[derive(Clone)]
52
+ pub(super) struct Registry {
53
+ collectors_by_id: Arc<RwLock<HashMap<u64, PromCollector>>>,
54
+ global_tags: BTreeMap<String, String>,
55
+ }
56
+
57
+ // A lot of this implementation code is lifted from the prometheus crate itself, and as such is a
58
+ // derivative work of the following:
59
+ // Copyright 2014 The Prometheus Authors
60
+ // Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
61
+ impl Registry {
62
+ pub(super) fn new(global_tags: HashMap<String, String>) -> Self {
63
+ Self {
64
+ collectors_by_id: Arc::new(RwLock::new(HashMap::new())),
65
+ global_tags: BTreeMap::from_iter(global_tags),
66
+ }
67
+ }
68
+
69
+ // Register a collector, potentially returning an existing collector that matches the provided
70
+ // one. In such cases the passed-in collector is discarded.
71
+ fn register<T: Into<PromCollector>>(&self, c: T) -> Option<PromCollector> {
72
+ let mut desc_id_set = HashSet::new();
73
+ let mut collector_id: u64 = 0;
74
+ let c = c.into();
75
+
76
+ for desc in c.desc() {
77
+ // If it is not a duplicate desc in this collector, add it to
78
+ // the collector_id. Here we assume that collectors (ie: metric vecs / histograms etc)
79
+ // should internally not repeat the same descriptor -- even though we allow entirely
80
+ // separate metrics with overlapping labels to be registered generally.
81
+ if desc_id_set.insert(desc.id) {
82
+ // Add the id and the dim hash, which includes both static and variable labels
83
+ collector_id = collector_id
84
+ .wrapping_add(desc.id)
85
+ .wrapping_add(desc.dim_hash);
86
+ } else {
87
+ dbg_panic!(
88
+ "Prometheus metric has duplicate descriptors, values may not be recorded on \
89
+ this metric. This is an SDK bug. Details: {:?}",
90
+ c.desc(),
91
+ );
92
+ return None;
93
+ }
94
+ }
95
+ match self.collectors_by_id.write().entry(collector_id) {
96
+ hash_map::Entry::Vacant(vc) => {
97
+ vc.insert(c);
98
+ None
99
+ }
100
+ hash_map::Entry::Occupied(o) => Some(o.get().clone()),
101
+ }
102
+ }
103
+
104
+ pub(super) fn gather(&self) -> Vec<MetricFamily> {
105
+ let mut mf_by_name = BTreeMap::new();
106
+
107
+ for c in self.collectors_by_id.read().values() {
108
+ let mfs = c.collect();
109
+ for mut mf in mfs {
110
+ if mf.get_metric().is_empty() {
111
+ continue;
112
+ }
113
+
114
+ let name = mf.name().to_owned();
115
+ match mf_by_name.entry(name) {
116
+ btree_map::Entry::Vacant(entry) => {
117
+ entry.insert(mf);
118
+ }
119
+ btree_map::Entry::Occupied(mut entry) => {
120
+ let existent_mf = entry.get_mut();
121
+ let existent_metrics = existent_mf.mut_metric();
122
+ for metric in mf.take_metric().into_iter() {
123
+ existent_metrics.push(metric);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ // Now that MetricFamilies are all set, sort their Metrics
131
+ // lexicographically by their label values.
132
+ for mf in mf_by_name.values_mut() {
133
+ mf.mut_metric().sort_by(|m1, m2| {
134
+ let lps1 = m1.get_label();
135
+ let lps2 = m2.get_label();
136
+
137
+ if lps1.len() != lps2.len() {
138
+ return lps1.len().cmp(&lps2.len());
139
+ }
140
+
141
+ for (lp1, lp2) in lps1.iter().zip(lps2.iter()) {
142
+ if lp1.value() != lp2.value() {
143
+ return lp1.value().cmp(lp2.value());
144
+ }
145
+ }
146
+
147
+ // We should never arrive here. Multiple metrics with the same
148
+ // label set in the same scrape will lead to undefined ingestion
149
+ // behavior. However, as above, we have to provide stable sorting
150
+ // here, even for inconsistent metrics. So sort equal metrics
151
+ // by their timestamp, with missing timestamps (implying "now")
152
+ // coming last.
153
+ m1.timestamp_ms().cmp(&m2.timestamp_ms())
154
+ });
155
+ }
156
+
157
+ mf_by_name
158
+ .into_values()
159
+ .map(|mut m| {
160
+ if self.global_tags.is_empty() {
161
+ return m;
162
+ }
163
+ // Add global labels
164
+ let pairs: Vec<LabelPair> = self
165
+ .global_tags
166
+ .iter()
167
+ .map(|(k, v)| {
168
+ let mut label = LabelPair::default();
169
+ label.set_name(k.to_string());
170
+ label.set_value(v.to_string());
171
+ label
172
+ })
173
+ .collect();
174
+
175
+ for metric in m.mut_metric().iter_mut() {
176
+ let mut labels: Vec<_> = metric.take_label();
177
+ labels.append(&mut pairs.clone());
178
+ metric.set_label(labels);
179
+ }
180
+ m
181
+ })
182
+ .collect()
183
+ }
184
+ }
185
+
186
+ impl Debug for Registry {
187
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
188
+ write!(
189
+ f,
190
+ "Registry({} collectors)",
191
+ self.collectors_by_id.read().keys().len()
192
+ )
193
+ }
194
+ }
195
+
196
+ #[derive(Debug)]
197
+ struct PromMetric<T> {
198
+ metric_name: String,
199
+ metric_description: String,
200
+ registry: Registry,
201
+ /// Map from label schema to the corresponding Prometheus vector metric
202
+ vectors: RwLock<HashMap<Vec<String>, T>>,
203
+ /// Bucket configuration for histograms (None for other metric types)
204
+ histogram_buckets: Option<Vec<f64>>,
205
+ }
206
+
207
+ impl<T> PromMetric<T>
208
+ where
209
+ T: Clone + Into<PromCollector> + TryFrom<PromCollector> + 'static,
210
+ {
211
+ fn new(metric_name: String, metric_description: String, registry: Registry) -> Self {
212
+ // Prometheus does not allow dashes
213
+ let metric_name = metric_name.replace('-', "_");
214
+ Self {
215
+ metric_name,
216
+ metric_description,
217
+ registry,
218
+ vectors: RwLock::new(HashMap::new()),
219
+ histogram_buckets: None,
220
+ }
221
+ }
222
+
223
+ fn get_or_create_vector<F>(&self, kvs: &OrderedPromLabelSet, create_fn: F) -> anyhow::Result<T>
224
+ where
225
+ F: FnOnce(&str, &str, &[&str]) -> anyhow::Result<T>,
226
+ {
227
+ // Just the metric label names
228
+ let schema: Vec<String> = kvs.keys_ordered().map(|kv| kv.to_string()).collect();
229
+
230
+ {
231
+ let vectors = self.vectors.read();
232
+ if let Some(vector) = vectors.get(&schema) {
233
+ return Ok(vector.clone());
234
+ }
235
+ }
236
+
237
+ let mut vectors = self.vectors.write();
238
+ // Double-check in case another thread created it
239
+ if let Some(vector) = vectors.get(&schema) {
240
+ return Ok(vector.clone());
241
+ }
242
+
243
+ let description = if self.metric_description.is_empty() {
244
+ &self.metric_name
245
+ } else {
246
+ &self.metric_description
247
+ };
248
+
249
+ let boxed: Box<[&str]> = schema.iter().map(String::as_str).collect();
250
+ let vector = create_fn(&self.metric_name, description, &boxed)?;
251
+
252
+ let maybe_exists = self.registry.register(vector.clone());
253
+ let vector = if let Some(m) = maybe_exists {
254
+ T::try_from(m).map_err(|_| {
255
+ anyhow!(
256
+ "Tried to register a metric that already exists as a different type: {:?}",
257
+ self.metric_name
258
+ )
259
+ })?
260
+ } else {
261
+ vector
262
+ };
263
+
264
+ vectors.insert(schema, vector.clone());
265
+ Ok(vector)
266
+ }
267
+ }
268
+
269
+ impl PromMetric<HistogramVec> {
270
+ fn new_with_buckets(
271
+ metric_name: String,
272
+ metric_description: String,
273
+ registry: Registry,
274
+ buckets: Vec<f64>,
275
+ ) -> Self {
276
+ // Prometheus does not allow dashes
277
+ let metric_name = metric_name.replace('-', "_");
278
+ Self {
279
+ metric_name,
280
+ metric_description,
281
+ registry,
282
+ vectors: RwLock::new(HashMap::new()),
283
+ histogram_buckets: Some(buckets),
284
+ }
285
+ }
286
+
287
+ fn get_or_create_vector_with_buckets(
288
+ &self,
289
+ labels: &OrderedPromLabelSet,
290
+ ) -> anyhow::Result<HistogramVec> {
291
+ self.get_or_create_vector(labels, |name, desc, label_names| {
292
+ let mut opts = prometheus::HistogramOpts::new(name, desc);
293
+ if let Some(buckets) = &self.histogram_buckets {
294
+ opts = opts.buckets(buckets.clone());
295
+ }
296
+ HistogramVec::new(opts, label_names).map_err(Into::into)
297
+ })
298
+ }
299
+ }
300
+
301
+ static EMPTY_LABEL_SET: OrderedPromLabelSet = OrderedPromLabelSet::new();
302
+
303
+ impl<T> PromMetric<T>
304
+ where
305
+ T: Clone + Collector + 'static,
306
+ {
307
+ fn extract_prometheus_labels<'a>(
308
+ &self,
309
+ attributes: &'a MetricAttributes,
310
+ ) -> anyhow::Result<&'a OrderedPromLabelSet, anyhow::Error> {
311
+ if matches!(attributes, MetricAttributes::Empty) {
312
+ return Ok(&EMPTY_LABEL_SET);
313
+ }
314
+ if let MetricAttributes::Prometheus { labels } = attributes {
315
+ Ok(labels)
316
+ } else {
317
+ let e = anyhow!(
318
+ "Must use Prometheus attributes with a Prometheus metric implementation. Got: {:?}",
319
+ attributes
320
+ );
321
+ dbg_panic!("{:?}", e);
322
+ Err(e)
323
+ }
324
+ }
325
+
326
+ fn label_mismatch_err(&self, attributes: &MetricAttributes) -> anyhow::Error {
327
+ let e = anyhow!(
328
+ "Mismatch between expected # of prometheus labels and provided for metric {}. \
329
+ This is an SDK bug. Attributes: {:?}",
330
+ self.metric_name,
331
+ attributes,
332
+ );
333
+ dbg_panic!("{:?}", e);
334
+ e
335
+ }
336
+ }
337
+
338
+ struct CorePromCounter(GenericCounter<prometheus::core::AtomicU64>);
339
+ impl CounterBase for CorePromCounter {
340
+ fn adds(&self, value: u64) {
341
+ self.0.inc_by(value);
342
+ }
343
+ }
344
+ impl MetricAttributable<Box<dyn CounterBase>> for PromMetric<IntCounterVec> {
345
+ fn with_attributes(
346
+ &self,
347
+ attributes: &MetricAttributes,
348
+ ) -> Result<Box<dyn CounterBase>, Box<dyn std::error::Error>> {
349
+ let labels = self.extract_prometheus_labels(attributes)?;
350
+ let vector = self.get_or_create_vector(labels, |name, desc, label_names| {
351
+ let opts = Opts::new(name, desc);
352
+ IntCounterVec::new(opts, label_names).map_err(Into::into)
353
+ })?;
354
+ if let Ok(c) = vector.get_metric_with(&labels.as_prom_labels()) {
355
+ Ok(Box::new(CorePromCounter(c)))
356
+ } else {
357
+ Err(self.label_mismatch_err(attributes).into())
358
+ }
359
+ }
360
+ }
361
+
362
+ struct CorePromIntGauge(prometheus::IntGauge);
363
+ impl GaugeBase for CorePromIntGauge {
364
+ fn records(&self, value: u64) {
365
+ self.0.set(value as i64);
366
+ }
367
+ }
368
+ impl MetricAttributable<Box<dyn GaugeBase>> for PromMetric<IntGaugeVec> {
369
+ fn with_attributes(
370
+ &self,
371
+ attributes: &MetricAttributes,
372
+ ) -> Result<Box<dyn GaugeBase>, Box<dyn std::error::Error>> {
373
+ let labels = self.extract_prometheus_labels(attributes)?;
374
+ let vector = self.get_or_create_vector(labels, |name, desc, label_names| {
375
+ let opts = Opts::new(name, desc);
376
+ IntGaugeVec::new(opts, label_names).map_err(Into::into)
377
+ })?;
378
+ if let Ok(g) = vector.get_metric_with(&labels.as_prom_labels()) {
379
+ Ok(Box::new(CorePromIntGauge(g)))
380
+ } else {
381
+ Err(self.label_mismatch_err(attributes).into())
382
+ }
383
+ }
384
+ }
385
+
386
+ struct CorePromGauge(prometheus::Gauge);
387
+ impl GaugeF64Base for CorePromGauge {
388
+ fn records(&self, value: f64) {
389
+ self.0.set(value);
390
+ }
391
+ }
392
+ impl MetricAttributable<Box<dyn GaugeF64Base>> for PromMetric<GaugeVec> {
393
+ fn with_attributes(
394
+ &self,
395
+ attributes: &MetricAttributes,
396
+ ) -> Result<Box<dyn GaugeF64Base>, Box<dyn std::error::Error>> {
397
+ let labels = self.extract_prometheus_labels(attributes)?;
398
+ let vector = self.get_or_create_vector(labels, |name, desc, label_names| {
399
+ let opts = Opts::new(name, desc);
400
+ GaugeVec::new(opts, label_names).map_err(Into::into)
401
+ })?;
402
+ if let Ok(g) = vector.get_metric_with(&labels.as_prom_labels()) {
403
+ Ok(Box::new(CorePromGauge(g)))
404
+ } else {
405
+ Err(self.label_mismatch_err(attributes).into())
406
+ }
407
+ }
408
+ }
409
+
410
+ #[derive(Clone)]
411
+ struct CorePromHistogram(prometheus::Histogram);
412
+ impl HistogramBase for CorePromHistogram {
413
+ fn records(&self, value: u64) {
414
+ self.0.observe(value as f64);
415
+ }
416
+ }
417
+ impl HistogramF64Base for CorePromHistogram {
418
+ fn records(&self, value: f64) {
419
+ self.0.observe(value);
420
+ }
421
+ }
422
+
423
+ #[derive(Debug)]
424
+ struct PromHistogramU64(PromMetric<HistogramVec>);
425
+ impl MetricAttributable<Box<dyn HistogramBase>> for PromHistogramU64 {
426
+ fn with_attributes(
427
+ &self,
428
+ attributes: &MetricAttributes,
429
+ ) -> Result<Box<dyn HistogramBase>, Box<dyn std::error::Error>> {
430
+ let labels = self.0.extract_prometheus_labels(attributes)?;
431
+ let vector = self.0.get_or_create_vector_with_buckets(labels)?;
432
+ if let Ok(h) = vector.get_metric_with(&labels.as_prom_labels()) {
433
+ Ok(Box::new(CorePromHistogram(h)))
434
+ } else {
435
+ Err(self.0.label_mismatch_err(attributes).into())
436
+ }
437
+ }
438
+ }
439
+
440
+ #[derive(Debug)]
441
+ struct PromHistogramF64(PromMetric<HistogramVec>);
442
+ impl MetricAttributable<Box<dyn HistogramF64Base>> for PromHistogramF64 {
443
+ fn with_attributes(
444
+ &self,
445
+ attributes: &MetricAttributes,
446
+ ) -> Result<Box<dyn HistogramF64Base>, Box<dyn std::error::Error>> {
447
+ let labels = self.0.extract_prometheus_labels(attributes)?;
448
+ let vector = self.0.get_or_create_vector_with_buckets(labels)?;
449
+ if let Ok(h) = vector.get_metric_with(&labels.as_prom_labels()) {
450
+ Ok(Box::new(CorePromHistogram(h)))
451
+ } else {
452
+ Err(self.0.label_mismatch_err(attributes).into())
453
+ }
454
+ }
455
+ }
456
+
457
+ /// A CoreMeter implementation backed by Prometheus metrics with dynamic label management
458
+ #[derive(Debug)]
459
+ pub struct CorePrometheusMeter {
460
+ registry: Registry,
461
+ use_seconds_for_durations: bool,
462
+ unit_suffix: bool,
463
+ bucket_overrides: temporal_sdk_core_api::telemetry::HistogramBucketOverrides,
464
+ }
465
+
466
+ impl CorePrometheusMeter {
467
+ pub(super) fn new(
468
+ registry: Registry,
469
+ use_seconds_for_durations: bool,
470
+ unit_suffix: bool,
471
+ bucket_overrides: temporal_sdk_core_api::telemetry::HistogramBucketOverrides,
472
+ ) -> Self {
473
+ Self {
474
+ registry,
475
+ use_seconds_for_durations,
476
+ unit_suffix,
477
+ bucket_overrides,
478
+ }
479
+ }
480
+
481
+ fn create_u64_hist(&self, params: &MetricParameters) -> PromHistogramU64 {
482
+ let base_name = params.name.to_string();
483
+ let actual_metric_name = self.get_histogram_metric_name(&base_name, &params.unit);
484
+ let buckets = self.get_buckets_for_metric(&base_name);
485
+ PromHistogramU64(PromMetric::new_with_buckets(
486
+ actual_metric_name,
487
+ params.description.to_string(),
488
+ self.registry.clone(),
489
+ buckets,
490
+ ))
491
+ }
492
+
493
+ fn create_f64_hist(&self, params: &MetricParameters) -> PromHistogramF64 {
494
+ let base_name = params.name.to_string();
495
+ let actual_metric_name = self.get_histogram_metric_name(&base_name, &params.unit);
496
+ let buckets = self.get_buckets_for_metric(&base_name);
497
+ PromHistogramF64(PromMetric::new_with_buckets(
498
+ actual_metric_name,
499
+ params.description.to_string(),
500
+ self.registry.clone(),
501
+ buckets,
502
+ ))
503
+ }
504
+ }
505
+
506
+ impl CoreMeter for CorePrometheusMeter {
507
+ fn new_attributes(&self, attribs: NewAttributes) -> MetricAttributes {
508
+ MetricAttributes::Prometheus {
509
+ labels: Arc::new(attribs.into()),
510
+ }
511
+ }
512
+
513
+ fn extend_attributes(
514
+ &self,
515
+ existing: MetricAttributes,
516
+ new: NewAttributes,
517
+ ) -> MetricAttributes {
518
+ if let MetricAttributes::Prometheus {
519
+ labels: existing_labels,
520
+ } = existing
521
+ {
522
+ let mut all_labels = Arc::unwrap_or_clone(existing_labels);
523
+ for kv in new.attributes.into_iter() {
524
+ all_labels.add_kv(kv);
525
+ }
526
+ MetricAttributes::Prometheus {
527
+ labels: Arc::new(all_labels),
528
+ }
529
+ } else {
530
+ dbg_panic!("Must use Prometheus attributes with a Prometheus metric implementation");
531
+ self.new_attributes(new)
532
+ }
533
+ }
534
+
535
+ fn counter(&self, params: MetricParameters) -> Counter {
536
+ let metric_name = params.name.to_string();
537
+ Counter::new(Arc::new(PromMetric::<IntCounterVec>::new(
538
+ metric_name,
539
+ params.description.to_string(),
540
+ self.registry.clone(),
541
+ )))
542
+ }
543
+
544
+ fn histogram(&self, params: MetricParameters) -> Histogram {
545
+ let hist = self.create_u64_hist(&params);
546
+ Histogram::new(Arc::new(hist))
547
+ }
548
+
549
+ fn histogram_f64(&self, params: MetricParameters) -> HistogramF64 {
550
+ let hist = self.create_f64_hist(&params);
551
+ HistogramF64::new(Arc::new(hist))
552
+ }
553
+
554
+ fn histogram_duration(&self, mut params: MetricParameters) -> HistogramDuration {
555
+ HistogramDuration::new(Arc::new(if self.use_seconds_for_durations {
556
+ params.unit = "seconds".into();
557
+ DurationHistogram::Seconds(self.create_f64_hist(&params))
558
+ } else {
559
+ params.unit = "milliseconds".into();
560
+ DurationHistogram::Milliseconds(self.create_u64_hist(&params))
561
+ }))
562
+ }
563
+
564
+ fn gauge(&self, params: MetricParameters) -> Gauge {
565
+ let metric_name = params.name.to_string();
566
+ Gauge::new(Arc::new(PromMetric::<IntGaugeVec>::new(
567
+ metric_name,
568
+ params.description.to_string(),
569
+ self.registry.clone(),
570
+ )))
571
+ }
572
+
573
+ fn gauge_f64(&self, params: MetricParameters) -> GaugeF64 {
574
+ let metric_name = params.name.to_string();
575
+ GaugeF64::new(Arc::new(PromMetric::<GaugeVec>::new(
576
+ metric_name,
577
+ params.description.to_string(),
578
+ self.registry.clone(),
579
+ )))
580
+ }
581
+ }
582
+
583
+ impl CorePrometheusMeter {
584
+ fn get_buckets_for_metric(&self, metric_name: &str) -> Vec<f64> {
585
+ for (name_pattern, buckets) in &self.bucket_overrides.overrides {
586
+ if metric_name.contains(name_pattern) {
587
+ return buckets.clone();
588
+ }
589
+ }
590
+ let base_metric_name = metric_name.strip_prefix("temporal_").unwrap_or(metric_name);
591
+ default_buckets_for(base_metric_name, self.use_seconds_for_durations).to_vec()
592
+ }
593
+
594
+ fn get_histogram_metric_name(&self, base_name: &str, unit: &str) -> String {
595
+ if self.unit_suffix && !unit.is_empty() {
596
+ format!("{base_name}_{unit}")
597
+ } else {
598
+ base_name.to_string()
599
+ }
600
+ }
601
+ }
602
+
603
+ enum DurationHistogram {
604
+ Milliseconds(PromHistogramU64),
605
+ Seconds(PromHistogramF64),
606
+ }
607
+
608
+ enum DurationHistogramBase {
609
+ Millis(Box<dyn HistogramBase>),
610
+ Secs(Box<dyn HistogramF64Base>),
611
+ }
612
+
613
+ impl HistogramDurationBase for DurationHistogramBase {
614
+ fn records(&self, value: Duration) {
615
+ match self {
616
+ DurationHistogramBase::Millis(h) => h.records(value.as_millis() as u64),
617
+ DurationHistogramBase::Secs(h) => h.records(value.as_secs_f64()),
618
+ }
619
+ }
620
+ }
621
+ impl MetricAttributable<Box<dyn HistogramDurationBase>> for DurationHistogram {
622
+ fn with_attributes(
623
+ &self,
624
+ attributes: &MetricAttributes,
625
+ ) -> Result<Box<dyn HistogramDurationBase>, Box<dyn std::error::Error>> {
626
+ Ok(match self {
627
+ DurationHistogram::Milliseconds(h) => Box::new(DurationHistogramBase::Millis(
628
+ h.with_attributes(attributes)?,
629
+ )),
630
+ DurationHistogram::Seconds(h) => {
631
+ Box::new(DurationHistogramBase::Secs(h.with_attributes(attributes)?))
632
+ }
633
+ })
634
+ }
635
+ }
636
+
637
+ #[cfg(test)]
638
+ mod tests {
639
+ use super::*;
640
+ use crate::telemetry::{TelemetryInstance, metrics::MetricsContext};
641
+ use prometheus::{Encoder, TextEncoder};
642
+ use temporal_sdk_core_api::telemetry::{
643
+ METRIC_PREFIX,
644
+ metrics::{MetricKeyValue, NewAttributes},
645
+ };
646
+
647
+ #[test]
648
+ fn test_prometheus_meter_dynamic_labels() {
649
+ let registry = Registry::new(HashMap::from([("global".to_string(), "value".to_string())]));
650
+ let meter = CorePrometheusMeter::new(
651
+ registry.clone(),
652
+ false,
653
+ false,
654
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
655
+ );
656
+
657
+ let counter = meter.counter(MetricParameters {
658
+ name: "test_counter".into(),
659
+ description: "A test counter metric".into(),
660
+ unit: "".into(),
661
+ });
662
+
663
+ let attrs1 = meter.new_attributes(NewAttributes::new(vec![
664
+ MetricKeyValue::new("service", "service1"),
665
+ MetricKeyValue::new("method", "get"),
666
+ ]));
667
+ counter.add(5, &attrs1);
668
+
669
+ let attrs2 = meter.new_attributes(NewAttributes::new(vec![
670
+ MetricKeyValue::new("service", "service2"),
671
+ MetricKeyValue::new("method", "post"),
672
+ ]));
673
+ counter.add(3, &attrs2);
674
+
675
+ let output = output_string(&registry);
676
+
677
+ // Both label combinations should be present
678
+ assert!(
679
+ output.contains("test_counter{method=\"get\",service=\"service1\",global=\"value\"} 5")
680
+ );
681
+ assert!(
682
+ output
683
+ .contains("test_counter{method=\"post\",service=\"service2\",global=\"value\"} 3")
684
+ );
685
+ }
686
+
687
+ #[test]
688
+ fn test_extend_attributes() {
689
+ let registry = Registry::new(HashMap::new());
690
+ let meter = CorePrometheusMeter::new(
691
+ registry.clone(),
692
+ false,
693
+ false,
694
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
695
+ );
696
+
697
+ let base_attrs = meter.new_attributes(NewAttributes::new(vec![
698
+ MetricKeyValue::new("service", "my_service"),
699
+ MetricKeyValue::new("version", "1.0"),
700
+ ]));
701
+
702
+ let extended_attrs = meter.extend_attributes(
703
+ base_attrs,
704
+ NewAttributes::new(vec![
705
+ MetricKeyValue::new("method", "GET"),
706
+ MetricKeyValue::new("version", "2.0"), // This should override
707
+ ]),
708
+ );
709
+
710
+ let counter = meter.counter(MetricParameters {
711
+ name: "test_extended".into(),
712
+ description: "Test extended attributes".into(),
713
+ unit: "".into(),
714
+ });
715
+ counter.add(1, &extended_attrs);
716
+
717
+ let output = output_string(&registry);
718
+
719
+ assert!(output.contains("service=\"my_service\""));
720
+ assert!(output.contains("method=\"GET\""));
721
+ assert!(output.contains("version=\"2.0\""));
722
+ assert!(!output.contains("version=\"1.0\""));
723
+ }
724
+
725
+ #[test]
726
+ fn test_workflow_e2e_latency_buckets() {
727
+ let registry = Registry::new(HashMap::new());
728
+
729
+ let meter_ms = CorePrometheusMeter::new(
730
+ registry.clone(),
731
+ false,
732
+ false,
733
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
734
+ );
735
+
736
+ let histogram_ms = meter_ms.histogram_duration(MetricParameters {
737
+ name: format!(
738
+ "temporal_{}",
739
+ crate::telemetry::WORKFLOW_E2E_LATENCY_HISTOGRAM_NAME
740
+ )
741
+ .into(),
742
+ description: "Test workflow e2e latency".into(),
743
+ unit: "duration".into(),
744
+ });
745
+ let attrs = meter_ms.new_attributes(NewAttributes::new(vec![]));
746
+ histogram_ms.record(Duration::from_millis(100), &attrs);
747
+
748
+ let output = output_string(&registry);
749
+
750
+ println!("Milliseconds histogram output:\n{output}");
751
+
752
+ assert!(
753
+ output.contains("le=\"100\""),
754
+ "Missing le=\"100\" bucket in milliseconds output"
755
+ );
756
+
757
+ // Test seconds configuration
758
+ let registry_s = Registry::new(HashMap::new());
759
+ let meter_s = CorePrometheusMeter::new(
760
+ registry_s.clone(),
761
+ true,
762
+ false,
763
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
764
+ );
765
+
766
+ let histogram_s = meter_s.histogram_duration(MetricParameters {
767
+ name: format!(
768
+ "temporal_{}",
769
+ crate::telemetry::WORKFLOW_E2E_LATENCY_HISTOGRAM_NAME
770
+ )
771
+ .into(),
772
+ description: "Test workflow e2e latency".into(),
773
+ unit: "duration".into(),
774
+ });
775
+ let attrs_s = meter_s.new_attributes(NewAttributes::new(vec![]));
776
+ histogram_s.record(Duration::from_millis(100), &attrs_s);
777
+
778
+ let output_s = output_string(&registry_s);
779
+
780
+ println!("Seconds histogram output:\n{output_s}");
781
+
782
+ assert!(
783
+ output_s.contains("le=\"0.1\""),
784
+ "Missing le=\"0.1\" bucket in seconds output"
785
+ );
786
+ }
787
+
788
+ #[test]
789
+ fn can_record_with_no_labels() {
790
+ let registry = Registry::new(HashMap::new());
791
+ let meter = CorePrometheusMeter::new(
792
+ registry.clone(),
793
+ false,
794
+ false,
795
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
796
+ );
797
+ let counter = meter.counter(MetricParameters {
798
+ name: "no_labels".into(),
799
+ description: "No labels".into(),
800
+ unit: "".into(),
801
+ });
802
+ counter.adds(1);
803
+
804
+ let output = output_string(&registry);
805
+
806
+ assert!(output.contains("no_labels 1"));
807
+ }
808
+
809
+ #[test]
810
+ fn works_with_recreated_metrics_context() {
811
+ let registry = Registry::new(HashMap::new());
812
+ let meter = CorePrometheusMeter::new(
813
+ registry.clone(),
814
+ false,
815
+ false,
816
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
817
+ );
818
+ let telem_instance = TelemetryInstance::new(
819
+ None,
820
+ None,
821
+ METRIC_PREFIX.to_string(),
822
+ Some(Arc::new(meter)),
823
+ true,
824
+ );
825
+ let mc = MetricsContext::top_level("foo".to_string(), "q".to_string(), &telem_instance);
826
+ mc.worker_registered();
827
+ drop(mc);
828
+
829
+ let mc = MetricsContext::top_level("foo".to_string(), "q".to_string(), &telem_instance);
830
+ mc.worker_registered();
831
+
832
+ let mc = MetricsContext::top_level("foo".to_string(), "q2".to_string(), &telem_instance);
833
+ mc.worker_registered();
834
+
835
+ let output = output_string(&registry);
836
+ assert!(output.contains("temporal_worker_start{namespace=\"foo\",service_name=\"temporal-core-sdk\",task_queue=\"q\"} 2"));
837
+ assert!(output.contains("temporal_worker_start{namespace=\"foo\",service_name=\"temporal-core-sdk\",task_queue=\"q2\"} 1"));
838
+ }
839
+
840
+ #[test]
841
+ fn metric_name_dashes() {
842
+ let registry = Registry::new(HashMap::new());
843
+ let meter = CorePrometheusMeter::new(
844
+ registry.clone(),
845
+ false,
846
+ false,
847
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
848
+ );
849
+ let dashes = meter.counter(MetricParameters {
850
+ name: "dash-in-name".into(),
851
+ description: "Whatever".into(),
852
+ unit: "".into(),
853
+ });
854
+ dashes.adds(1);
855
+
856
+ let output = output_string(&registry);
857
+ assert!(output.contains("dash_in_name 1"));
858
+ }
859
+
860
+ #[test]
861
+ #[should_panic(expected = "not a valid metric name")]
862
+ fn invalid_metric_name() {
863
+ let registry = Registry::new(HashMap::new());
864
+ let meter = CorePrometheusMeter::new(
865
+ registry.clone(),
866
+ false,
867
+ false,
868
+ temporal_sdk_core_api::telemetry::HistogramBucketOverrides::default(),
869
+ );
870
+ let dashes = meter.counter(MetricParameters {
871
+ name: "not@permitted:symbols".into(),
872
+ description: "Whatever".into(),
873
+ unit: "".into(),
874
+ });
875
+ dashes.adds(1);
876
+ }
877
+
878
+ fn output_string(registry: &Registry) -> String {
879
+ let metric_families = registry.gather();
880
+ let encoder = TextEncoder::new();
881
+ let mut buffer = vec![];
882
+ encoder.encode(&metric_families, &mut buffer).unwrap();
883
+ String::from_utf8(buffer).unwrap()
884
+ }
885
+ }