1import * as internal from "@effect/opentelemetry/internal/metrics"
2import * as OtelMetrics from "@effect/opentelemetry/OtelMetrics"
3import { assert, describe, it } from "@effect/vitest"
4import { ValueType } from "@opentelemetry/api"
5import { resourceFromAttributes } from "@opentelemetry/resources"
6import { MetricReader } from "@opentelemetry/sdk-metrics"
7import * as Effect from "effect/Effect"
8import * as Metric from "effect/Metric"
9
10const findMetric = (metrics: any, name: string) =>
11 metrics.resourceMetrics.scopeMetrics[0].metrics.find((_: any) => _.descriptor.name === name)
12
13describe("Metrics", () => {
14 it.effect("gauge", () =>
15 Effect.gen(function*() {
16 const services = yield* Effect.context<never>()
17 const resource = resourceFromAttributes({
18 name: "test",
19 version: "1.0.0"
20 })
21 const producer = new internal.MetricProducerImpl(resource, services)
22 const gauge = Metric.gauge("rps")
23
24 yield* Metric.withAttributes(gauge, {
25 key: "value",
26 unit: "requests"
27 }).pipe(Metric.update(10))
28 yield* Metric.withAttributes(gauge, {
29 key: "value"
30 }).pipe(Metric.update(10))
31 yield* Metric.withAttributes(gauge, {
32 key: "value"
33 }).pipe(Metric.update(20))
34
35 const results = yield* Effect.promise(() => producer.collect())
36 const object = JSON.parse(JSON.stringify(results))
37 assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
38 ["name", "test"],
39 ["version", "1.0.0"]
40 ])
41 assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
42 const metric = findMetric(object, "rps")
43 assert.deepStrictEqual(metric, {
44 "dataPointType": 2,
45 "descriptor": {
46 "advice": {},
47 "name": "rps",
48 "description": "",
49 "unit": "requests",
50 "type": "OBSERVABLE_GAUGE",
51 "valueType": ValueType.DOUBLE
52 },
53 "aggregationTemporality": 1,
54 "dataPoints": [
55 {
56 "startTime": metric.dataPoints[0].startTime,
57 "endTime": metric.dataPoints[0].endTime,
58 "attributes": {
59 "unit": "requests",
60 "key": "value"
61 },
62 "value": 10
63 },
64 {
65 "startTime": metric.dataPoints[0].startTime,
66 "endTime": metric.dataPoints[0].endTime,
67 "attributes": {
68 "key": "value"
69 },
70 "value": 20
71 }
72 ]
73 })
74 }))
75
76 it.effect("gauge bigint", () =>
77 Effect.gen(function*() {
78 const services = yield* Effect.context<never>()
79 const producer = new internal.MetricProducerImpl(
80 resourceFromAttributes({
81 name: "test",
82 version: "1.0.0"
83 }),
84 services
85 )
86 const gauge = Metric.gauge("rps-bigint", { bigint: true })
87
88 yield* Metric.withAttributes(gauge, {
89 key: "value",
90 unit: "requests"
91 }).pipe(Metric.update(10n))
92 yield* Metric.withAttributes(gauge, {
93 key: "value"
94 }).pipe(Metric.update(10n))
95 yield* Metric.withAttributes(gauge, {
96 key: "value"
97 }).pipe(Metric.update(20n))
98
99 const results = yield* Effect.promise(() => producer.collect())
100 const object = JSON.parse(JSON.stringify(results))
101 assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
102 ["name", "test"],
103 ["version", "1.0.0"]
104 ])
105 assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
106 const metric = findMetric(object, "rps-bigint")
107 assert.deepEqual(metric, {
108 "dataPointType": 2,
109 "descriptor": {
110 "advice": {},
111 "name": "rps-bigint",
112 "description": "",
113 "unit": "requests",
114 "type": "OBSERVABLE_GAUGE",
115 "valueType": ValueType.INT
116 },
117 "aggregationTemporality": 1,
118 "dataPoints": [
119 {
120 "startTime": metric.dataPoints[0].startTime,
121 "endTime": metric.dataPoints[0].endTime,
122 "attributes": {
123 "unit": "requests",
124 "key": "value"
125 },
126 "value": 10
127 },
128 {
129 "startTime": metric.dataPoints[0].startTime,
130 "endTime": metric.dataPoints[0].endTime,
131 "attributes": {
132 "key": "value"
133 },
134 "value": 20
135 }
136 ]
137 })
138 }))
139
140 it.effect("counter", () =>
141 Effect.gen(function*() {
142 const services = yield* Effect.context<never>()
143 const producer = new internal.MetricProducerImpl(
144 resourceFromAttributes({
145 name: "test",
146 version: "1.0.0"
147 }),
148 services
149 )
150 const counter = Metric.counter("counter", { description: "Example" })
151
152 yield* Metric.withAttributes(counter, {
153 key: "value",
154 unit: "requests"
155 }).pipe(Metric.update(1))
156 yield* Metric.withAttributes(counter, {
157 key: "value"
158 }).pipe(Metric.update(1))
159 yield* Metric.withAttributes(counter, {
160 key: "value"
161 }).pipe(Metric.update(1))
162
163 const results = yield* Effect.promise(() => producer.collect())
164 const object = JSON.parse(JSON.stringify(results))
165 assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
166 ["name", "test"],
167 ["version", "1.0.0"]
168 ])
169 assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
170 const metric = findMetric(object, "counter")
171 assert.deepEqual(metric, {
172 "dataPointType": 3,
173 "descriptor": {
174 "advice": {},
175 "name": "counter",
176 "description": "Example",
177 "unit": "requests",
178 "type": "UP_DOWN_COUNTER",
179 "valueType": ValueType.DOUBLE
180 },
181 "isMonotonic": false,
182 "aggregationTemporality": 1,
183 "dataPoints": [
184 {
185 "startTime": metric.dataPoints[0].startTime,
186 "endTime": metric.dataPoints[0].endTime,
187 "attributes": {
188 "unit": "requests",
189 "key": "value"
190 },
191 "value": 1
192 },
193 {
194 "startTime": metric.dataPoints[0].startTime,
195 "endTime": metric.dataPoints[0].endTime,
196 "attributes": {
197 "key": "value"
198 },
199 "value": 2
200 }
201 ]
202 })
203 }))
204
205 it.effect("counter-inc", () =>
206 Effect.gen(function*() {
207 const services = yield* Effect.context<never>()
208 const producer = new internal.MetricProducerImpl(
209 resourceFromAttributes({
210 name: "test",
211 version: "1.0.0"
212 }),
213 services
214 )
215 const counter = Metric.counter("counter-inc", {
216 description: "Example",
217 incremental: true
218 })
219
220 yield* Metric.withAttributes(counter, {
221 key: "value",
222 unit: "requests"
223 }).pipe(Metric.update(1))
224 yield* Metric.withAttributes(counter, {
225 key: "value"
226 }).pipe(Metric.update(1))
227 yield* Metric.withAttributes(counter, {
228 key: "value"
229 }).pipe(Metric.update(1))
230
231 const results = yield* Effect.promise(() => producer.collect())
232 const object = JSON.parse(JSON.stringify(results))
233 assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
234 ["name", "test"],
235 ["version", "1.0.0"]
236 ])
237 assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
238 const metric = findMetric(object, "counter-inc")
239 assert.deepEqual(metric, {
240 "dataPointType": 3,
241 "descriptor": {
242 "advice": {},
243 "name": "counter-inc",
244 "description": "Example",
245 "unit": "requests",
246 "type": "COUNTER",
247 "valueType": ValueType.DOUBLE
248 },
249 "isMonotonic": true,
250 "aggregationTemporality": 1,
251 "dataPoints": [
252 {
253 "startTime": metric.dataPoints[0].startTime,
254 "endTime": metric.dataPoints[0].endTime,
255 "attributes": {
256 "unit": "requests",
257 "key": "value"
258 },
259 "value": 1
260 },
261 {
262 "startTime": metric.dataPoints[0].startTime,
263 "endTime": metric.dataPoints[0].endTime,
264 "attributes": {
265 "key": "value"
266 },
267 "value": 2
268 }
269 ]
270 })
271 }))
272
273 it.effect("counter-bigint", () =>
274 Effect.gen(function*() {
275 const services = yield* Effect.context<never>()
276 const producer = new internal.MetricProducerImpl(
277 resourceFromAttributes({
278 name: "test",
279 version: "1.0.0"
280 }),
281 services
282 )
283 const counter = Metric.counter("counter-bigint", {
284 description: "Example",
285 incremental: true,
286 bigint: true
287 })
288
289 yield* Metric.withAttributes(counter, {
290 key: "value",
291 unit: "requests"
292 }).pipe(Metric.update(1n))
293 yield* Metric.withAttributes(counter, {
294 key: "value"
295 }).pipe(Metric.update(1n))
296 yield* Metric.withAttributes(counter, {
297 key: "value"
298 }).pipe(Metric.update(1n))
299
300 const results = yield* Effect.promise(() => producer.collect())
301 const object = JSON.parse(JSON.stringify(results))
302 assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
303 ["name", "test"],
304 ["version", "1.0.0"]
305 ])
306 assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
307 const metric = findMetric(object, "counter-bigint")
308 assert.deepEqual(metric, {
309 "dataPointType": 3,
310 "descriptor": {
311 "advice": {},
312 "name": "counter-bigint",
313 "description": "Example",
314 "unit": "requests",
315 "type": "COUNTER",
316 "valueType": ValueType.INT
317 },
318 "isMonotonic": true,
319 "aggregationTemporality": 1,
320 "dataPoints": [
321 {
322 "startTime": metric.dataPoints[0].startTime,
323 "endTime": metric.dataPoints[0].endTime,
324 "attributes": {
325 "unit": "requests",
326 "key": "value"
327 },
328 "value": 1
329 },
330 {
331 "startTime": metric.dataPoints[0].startTime,
332 "endTime": metric.dataPoints[0].endTime,
333 "attributes": {
334 "key": "value"
335 },
336 "value": 2
337 }
338 ]
339 })
340 }))
341
342 it.effect("reports the same delta to every registered reader", () =>
343 Effect.gen(function*() {
344 class Reader extends MetricReader {
345 protected onShutdown(): Promise<void> {
346 return Promise.resolve()
347 }
348 protected onForceFlush(): Promise<void> {
349 return Promise.resolve()
350 }
351 }
352
353 const services = yield* Effect.context<never>()
354 const producer = new internal.MetricProducerImpl(resourceFromAttributes({}), services, "delta")
355 const first = new Reader()
356 const second = new Reader()
357 yield* OtelMetrics.registerProducer(producer, () => [first, second])
358 yield* Metric.update(Metric.counter("requests", { incremental: true }), 1)
359
360 const firstResult = yield* Effect.promise(() => first.collect())
361 const secondResult = yield* Effect.promise(() => second.collect())
362 const firstValue = (firstResult.resourceMetrics.scopeMetrics[0]!.metrics[0] as any).dataPoints[0].value
363 const secondValue = (secondResult.resourceMetrics.scopeMetrics[0]!.metrics[0] as any).dataPoints[0].value
364 assert.deepStrictEqual([firstValue, secondValue], [1, 1])
365 }).pipe(Effect.provideService(Metric.MetricRegistry, new Map())))
366})