|
6 | 6 | runtime.go.*, runtime.node.*, etc.). |
7 | 7 | """ |
8 | 8 |
|
| 9 | +from typing import TypedDict |
| 10 | + |
9 | 11 | from utils import context, features, interfaces, scenarios, weblog |
10 | 12 |
|
11 | 13 |
|
12 | | -# Maps each expected metric to its attribute constraints: |
13 | | -# "all" — attribute keys that must appear on every emitted data point. |
14 | | -# "some" — attribute keys that must appear on at least one data point. |
15 | | -# |
16 | | -# Use "some" when a metric emits both per-dimension points (which carry the attribute) |
17 | | -# and aggregate rollup points (which don't). For example, jvm.memory.used emits one |
18 | | -# point per memory pool (with jvm.memory.pool.name) as well as heap/non-heap totals |
19 | | -# (without it), so pool.name goes in "some" while jvm.memory.type, which is present |
20 | | -# on every point, goes in "all". |
21 | | -EXPECTED_METRICS: dict[str, dict[str, dict[str, list[str]]]] = { |
| 14 | +class MetricConstraints(TypedDict, total=False): |
| 15 | + """Attribute constraints for an expected metric. |
| 16 | +
|
| 17 | + all: keys required on every data point. |
| 18 | + some: keys required on at least one data point. |
| 19 | + present_values: attribute -> values that must each appear on at least one data point. |
| 20 | + """ |
| 21 | + |
| 22 | + all: list[str] |
| 23 | + some: list[str] |
| 24 | + present_values: dict[str, list[str]] |
| 25 | + |
| 26 | + |
| 27 | +# Maps each expected metric to its attribute constraints (see MetricConstraints). |
| 28 | +EXPECTED_METRICS: dict[str, dict[str, MetricConstraints]] = { |
22 | 29 | "dotnet": { |
23 | 30 | "dotnet.assembly.count": {"all": []}, |
24 | 31 | "dotnet.exceptions": {"all": []}, |
|
72 | 79 | "v8js.memory.heap.space.physical_size": {"all": ["v8js.heap.space.name"]}, |
73 | 80 | "v8js.memory.heap.space.size": {"all": ["v8js.heap.space.name"]}, |
74 | 81 | "v8js.memory.heap.used": {"all": ["v8js.heap.space.name"]}, |
75 | | - "v8js.resource.active": {"all": ["v8js.resource.type"]}, |
| 82 | + # v8js.resource.type is open-ended, so assert a known value is present instead of using a |
| 83 | + # closed allow-list: the weblog's listening HTTP server always emits TCPServerWrap. |
| 84 | + "v8js.resource.active": { |
| 85 | + "all": ["v8js.resource.type"], |
| 86 | + "present_values": {"v8js.resource.type": ["TCPServerWrap"]}, |
| 87 | + }, |
76 | 88 | }, |
77 | 89 | "java": { |
78 | 90 | "jvm.buffer.count": {"all": ["jvm.buffer.pool.name"]}, |
|
108 | 120 |
|
109 | 121 | # Valid value domains for attributes. For closed enums (jvm.memory.type, jvm.thread.*, |
110 | 122 | # nodejs.eventloop.state, v8js.gc.type) these are exhaustive. For open-ended attributes |
111 | | -# (pool names, GC names, V8 heap space names, libuv resource types) these are supersets |
112 | | -# covering all known implementations — the assertion is that observed values fall within |
113 | | -# the known universe. |
| 123 | +# (pool names, GC names, V8 heap space names) these are supersets covering all known |
| 124 | +# implementations — the assertion is that observed values fall within the known universe. |
114 | 125 | EXPECTED_METRIC_ATTRIBUTE_VALUES: dict[str, dict[str, frozenset[str]]] = { |
115 | 126 | "nodejs": { |
116 | 127 | # Closed enum: performance.eventLoopUtilization() exposes idle and active only. |
|
138 | 149 | "shared_trusted_large_object_space", |
139 | 150 | } |
140 | 151 | ), |
141 | | - # libuv handle types reported by process.getActiveResourcesInfo(); superset across Node 18+. |
142 | | - "v8js.resource.type": frozenset( |
143 | | - { |
144 | | - "Immediate", |
145 | | - "Timeout", |
146 | | - "TCPServerWrap", |
147 | | - "TCPWrap", |
148 | | - "TTYWrap", |
149 | | - "PipeWrap", |
150 | | - "UDPWrap", |
151 | | - "TLSWrap", |
152 | | - "FSReqCallback", |
153 | | - "MessagePort", |
154 | | - "DNSChannel", |
155 | | - "FSEvent", |
156 | | - "SignalWrap", |
157 | | - "StatWatcher", |
158 | | - "HTTPClientRequest", |
159 | | - "HTTPParser", |
160 | | - "Microtask", |
161 | | - } |
162 | | - ), |
| 152 | + # v8js.resource.type is open-ended (validated via present_values in EXPECTED_METRICS, not here). |
163 | 153 | }, |
164 | 154 | "java": { |
165 | 155 | "jvm.memory.type": frozenset({"heap", "non_heap"}), |
@@ -314,6 +304,15 @@ def test_otel_metrics_are_present_and_attributed(self) -> None: |
314 | 304 | f"Expected one of: {sorted(attribute_values[key])}" |
315 | 305 | ) |
316 | 306 |
|
| 307 | + for attr_key, required_values in constraints.get("present_values", {}).items(): |
| 308 | + observed_values = {pt[attr_key] for pt in points if attr_key in pt} |
| 309 | + for required_value in required_values: |
| 310 | + assert required_value in observed_values, ( |
| 311 | + f"Metric '{metric_name}' expected at least one data point with " |
| 312 | + f"{attr_key}='{required_value}' for {library}. " |
| 313 | + f"Observed {attr_key} values: {sorted(observed_values)}" |
| 314 | + ) |
| 315 | + |
317 | 316 | def setup_dd_metrics_are_absent(self) -> None: |
318 | 317 | self.req = weblog.get("/") |
319 | 318 |
|
|
0 commit comments