@effect/opentelemetry 0.8.2 → 0.9.1

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.
@@ -0,0 +1,310 @@
1
+ import type { LazyArg } from "@effect/data/Function"
2
+ import * as HashSet from "@effect/data/HashSet"
3
+ import * as Option from "@effect/data/Option"
4
+ import * as Effect from "@effect/io/Effect"
5
+ import * as Layer from "@effect/io/Layer"
6
+ import * as Metric from "@effect/io/Metric"
7
+ import type * as MetricKey from "@effect/io/Metric/Key"
8
+ import * as MetricKeyType from "@effect/io/Metric/KeyType"
9
+ import * as MetricLabel from "@effect/io/Metric/Label"
10
+ import * as MetricState from "@effect/io/Metric/State"
11
+ import * as Resource from "@effect/opentelemetry/Resource"
12
+ import type { HrTime } from "@opentelemetry/api"
13
+ import { ValueType } from "@opentelemetry/api"
14
+ import type * as Resources from "@opentelemetry/resources"
15
+ import type {
16
+ CollectionResult,
17
+ DataPoint,
18
+ InstrumentDescriptor,
19
+ MetricData,
20
+ MetricReader
21
+ } from "@opentelemetry/sdk-metrics"
22
+ import { AggregationTemporality, DataPointType, InstrumentType } from "@opentelemetry/sdk-metrics"
23
+ import type { MetricCollectOptions, MetricProducer } from "@opentelemetry/sdk-metrics/build/src/export/MetricProducer"
24
+
25
+ const sdkName = "@effect/opentelemetry/Metrics"
26
+
27
+ const aggregationTemporalityLabelKey = `${sdkName}/aggregationTemporality`
28
+
29
+ /** @internal */
30
+ export const aggregationTemporalityLabel = (value: "delta" | "cumulative") =>
31
+ MetricLabel.make(
32
+ aggregationTemporalityLabelKey,
33
+ value
34
+ )
35
+
36
+ const incrementalOnlyLabelKey = `${sdkName}/isIncrementalOnly`
37
+
38
+ /** @internal */
39
+ export const incrementalOnlyLabel = MetricLabel.make(
40
+ incrementalOnlyLabelKey,
41
+ "true"
42
+ )
43
+
44
+ const integerLabelKey = `${sdkName}/isInteger`
45
+
46
+ /** @internal */
47
+ export const integerLabel = MetricLabel.make(
48
+ integerLabelKey,
49
+ "true"
50
+ )
51
+
52
+ const unitLabelKey = `${sdkName}/unit`
53
+
54
+ /** @internal */
55
+ export const unitLabel = (unit: string) =>
56
+ MetricLabel.make(
57
+ unitLabelKey,
58
+ unit
59
+ )
60
+
61
+ /** @internal */
62
+ export class MetricProducerImpl implements MetricProducer {
63
+ constructor(readonly resource: Resources.Resource) {
64
+ }
65
+
66
+ collect(_options?: MetricCollectOptions): Promise<CollectionResult> {
67
+ const snapshot = Metric.unsafeSnapshot()
68
+ const hrTimeNow = currentHrTime()
69
+ const metricData: Array<MetricData> = []
70
+
71
+ for (const { metricKey, metricState } of snapshot) {
72
+ const tags = HashSet.reduce(metricKey.tags, {}, (acc: Record<string, string>, label) => {
73
+ acc[label.key] = label.value
74
+ return acc
75
+ })
76
+ const attributes = Object.fromEntries(
77
+ Object.entries(tags).filter(([key]) => !key.startsWith(sdkName))
78
+ )
79
+ const isDelta = tags[aggregationTemporalityLabelKey] === "delta"
80
+ const descriptor = descriptorFromKey(metricKey, tags)
81
+
82
+ if (MetricState.isCounterState(metricState)) {
83
+ metricData.push({
84
+ dataPointType: DataPointType.SUM,
85
+ descriptor,
86
+ isMonotonic: descriptor.type === InstrumentType.COUNTER,
87
+ aggregationTemporality: isDelta ? AggregationTemporality.DELTA : AggregationTemporality.CUMULATIVE,
88
+ dataPoints: [{
89
+ startTime: hrTimeNow,
90
+ endTime: hrTimeNow,
91
+ attributes,
92
+ value: metricState.count
93
+ }]
94
+ })
95
+ } else if (MetricState.isGaugeState(metricState)) {
96
+ metricData.push({
97
+ dataPointType: DataPointType.GAUGE,
98
+ descriptor,
99
+ aggregationTemporality: isDelta ? AggregationTemporality.DELTA : AggregationTemporality.CUMULATIVE,
100
+ dataPoints: [{
101
+ startTime: hrTimeNow,
102
+ endTime: hrTimeNow,
103
+ attributes,
104
+ value: metricState.value
105
+ }]
106
+ })
107
+ } else if (MetricState.isHistogramState(metricState)) {
108
+ const size = metricState.buckets.length
109
+ const buckets = {
110
+ boundaries: Array<number>(size - 1),
111
+ counts: Array<number>(size)
112
+ }
113
+ let i = 0
114
+ let prev = 0
115
+ for (const [boundary, value] of metricState.buckets) {
116
+ if (i < size - 1) {
117
+ buckets.boundaries[i] = boundary
118
+ }
119
+ buckets.counts[i] = value - prev
120
+ prev = value
121
+ i++
122
+ }
123
+
124
+ metricData.push({
125
+ dataPointType: DataPointType.HISTOGRAM,
126
+ descriptor,
127
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
128
+ dataPoints: [{
129
+ startTime: hrTimeNow,
130
+ endTime: hrTimeNow,
131
+ attributes,
132
+ value: {
133
+ buckets,
134
+ count: metricState.count,
135
+ min: metricState.min,
136
+ max: metricState.max,
137
+ sum: metricState.sum
138
+ }
139
+ }]
140
+ })
141
+ } else if (MetricState.isFrequencyState(metricState)) {
142
+ const dataPoints: Array<DataPoint<number>> = []
143
+ for (const [freqKey, value] of metricState.occurrences) {
144
+ dataPoints.push({
145
+ startTime: hrTimeNow,
146
+ endTime: hrTimeNow,
147
+ attributes: {
148
+ ...attributes,
149
+ key: freqKey
150
+ },
151
+ value
152
+ })
153
+ }
154
+ metricData.push({
155
+ dataPointType: DataPointType.SUM,
156
+ descriptor: descriptorFromKey(metricKey, tags),
157
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
158
+ isMonotonic: true,
159
+ dataPoints
160
+ })
161
+ } else if (MetricState.isSummaryState(metricState)) {
162
+ const dataPoints: Array<DataPoint<number>> = [{
163
+ startTime: hrTimeNow,
164
+ endTime: hrTimeNow,
165
+ attributes: { ...attributes, quantile: "min" },
166
+ value: metricState.min
167
+ }]
168
+ for (const [quantile, value] of metricState.quantiles) {
169
+ dataPoints.push({
170
+ startTime: hrTimeNow,
171
+ endTime: hrTimeNow,
172
+ attributes: { ...attributes, quantile: quantile.toString() },
173
+ value: value._tag === "Some" ? value.value : 0
174
+ })
175
+ }
176
+ dataPoints.push({
177
+ startTime: hrTimeNow,
178
+ endTime: hrTimeNow,
179
+ attributes: { ...attributes, quantile: "max" },
180
+ value: metricState.max
181
+ })
182
+
183
+ metricData.push({
184
+ dataPointType: DataPointType.SUM,
185
+ descriptor: descriptorFromKey(metricKey, tags, "quantiles"),
186
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
187
+ isMonotonic: false,
188
+ dataPoints
189
+ })
190
+ metricData.push({
191
+ dataPointType: DataPointType.SUM,
192
+ descriptor: {
193
+ ...descriptorMeta(metricKey, "count"),
194
+ unit: "1",
195
+ type: InstrumentType.COUNTER,
196
+ valueType: ValueType.INT
197
+ },
198
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
199
+ isMonotonic: true,
200
+ dataPoints: [{
201
+ startTime: hrTimeNow,
202
+ endTime: hrTimeNow,
203
+ attributes,
204
+ value: metricState.count
205
+ }]
206
+ })
207
+ metricData.push({
208
+ dataPointType: DataPointType.SUM,
209
+ descriptor: {
210
+ ...descriptorMeta(metricKey, "sum"),
211
+ unit: "1",
212
+ type: InstrumentType.COUNTER,
213
+ valueType: ValueType.DOUBLE
214
+ },
215
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
216
+ isMonotonic: true,
217
+ dataPoints: [{
218
+ startTime: hrTimeNow,
219
+ endTime: hrTimeNow,
220
+ attributes,
221
+ value: metricState.sum
222
+ }]
223
+ })
224
+ }
225
+ }
226
+
227
+ return Promise.resolve({
228
+ resourceMetrics: {
229
+ resource: this.resource,
230
+ scopeMetrics: [{
231
+ scope: { name: sdkName },
232
+ metrics: metricData
233
+ }]
234
+ },
235
+ errors: []
236
+ })
237
+ }
238
+ }
239
+
240
+ const descriptorMeta = (
241
+ metricKey: MetricKey.MetricKey.Untyped,
242
+ suffix?: string
243
+ ) => ({
244
+ name: suffix ? `${metricKey.name}_${suffix}` : metricKey.name,
245
+ description: Option.getOrElse(metricKey.description, () => "")
246
+ })
247
+
248
+ const descriptorFromKey = (
249
+ metricKey: MetricKey.MetricKey.Untyped,
250
+ tags: Record<string, string>,
251
+ suffix?: string
252
+ ): InstrumentDescriptor => ({
253
+ ...descriptorMeta(metricKey, suffix),
254
+ unit: tags[unitLabelKey] ?? tags.unit ?? tags.time_unit ?? "1",
255
+ type: instrumentTypeFromKey(metricKey, tags),
256
+ valueType: valueTypeFromKey(metricKey, tags)
257
+ })
258
+
259
+ const instrumentTypeFromKey = (key: MetricKey.MetricKey.Untyped, tags: Record<string, string>): InstrumentType => {
260
+ if (MetricKeyType.isHistogramKey(key.keyType)) {
261
+ return InstrumentType.HISTOGRAM
262
+ } else if (MetricKeyType.isGaugeKey(key.keyType)) {
263
+ return InstrumentType.OBSERVABLE_GAUGE
264
+ } else if (MetricKeyType.isSummaryKey(key.keyType)) {
265
+ return InstrumentType.UP_DOWN_COUNTER
266
+ } else if (MetricKeyType.isFrequencyKey(key.keyType)) {
267
+ return InstrumentType.COUNTER
268
+ }
269
+
270
+ return tags[incrementalOnlyLabelKey] ?
271
+ InstrumentType.COUNTER :
272
+ InstrumentType.UP_DOWN_COUNTER
273
+ }
274
+
275
+ const valueTypeFromKey = (_key: MetricKey.MetricKey.Untyped, tags: Record<string, string>): ValueType => {
276
+ if (tags[integerLabelKey]) {
277
+ return ValueType.INT
278
+ }
279
+
280
+ return ValueType.DOUBLE
281
+ }
282
+
283
+ const currentHrTime = (): HrTime => {
284
+ const now = Date.now()
285
+ return [Math.floor(now / 1000), (now % 1000) * 1000000]
286
+ }
287
+
288
+ /** @internal */
289
+ export const makeProducer = Effect.map(
290
+ Resource.Resource,
291
+ (resource): MetricProducer => new MetricProducerImpl(resource)
292
+ )
293
+
294
+ /** @internal */
295
+ export const registerProducer = (self: MetricProducer, metricReader: LazyArg<MetricReader>) =>
296
+ Effect.acquireRelease(
297
+ Effect.sync(() => {
298
+ const reader = metricReader()
299
+ reader.setMetricProducer(self)
300
+ return reader
301
+ }),
302
+ (reader) => Effect.promise(() => reader.shutdown())
303
+ )
304
+
305
+ /** @internal */
306
+ export const layer = (evaluate: LazyArg<MetricReader>) =>
307
+ Layer.scopedDiscard(Effect.flatMap(
308
+ makeProducer,
309
+ (producer) => registerProducer(producer, evaluate)
310
+ ))
@@ -34,23 +34,18 @@ export class OtelSpan implements Tracer.Span {
34
34
  startTime: bigint
35
35
  ) {
36
36
  const active = contextApi.active()
37
- this.span = tracer.startSpan(
38
- name,
39
- {
40
- startTime: nanosToHrTime(startTime),
41
- links: links.length > 0 ?
42
- links.map((link) => ({
43
- context: makeSpanContext(link.span),
44
- attributes: link.attributes
45
- })) :
46
- undefined
47
- },
48
- parent._tag === "Some" ? populateContext(active, parent.value, context) : active
49
- )
37
+ this.span = tracer.startSpan(name, {
38
+ startTime: nanosToHrTime(startTime),
39
+ links: links.length > 0
40
+ ? links.map((link) => ({
41
+ context: makeSpanContext(link.span),
42
+ attributes: link.attributes
43
+ }))
44
+ : undefined
45
+ }, parent._tag === "Some" ? populateContext(active, parent.value, context) : active)
50
46
  const spanContext = this.span.spanContext()
51
47
  this.spanId = spanContext.spanId
52
48
  this.traceId = spanContext.traceId
53
-
54
49
  this.status = {
55
50
  _tag: "Started",
56
51
  startTime