@effect/opentelemetry 0.9.0 → 0.10.0

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