@smthrs/observability 1.0.0-rc.5 → 1.0.0-rc.7

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.
package/src/Otel.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  * @since 0.1.0
27
27
  */
28
28
  export interface Options {
29
- /** Validated resource for metrics, tracer scope, and provider factories. */
29
+ /** Validated resource for metrics and provider factories. */
30
30
  readonly resource: ResourceConfiguration
31
31
  /**
32
32
  * Borrowed: the layer bridges onto this provider and never flushes or shuts
@@ -60,7 +60,8 @@ export interface Options {
60
60
  * layer acquires. Factories run during layer acquisition after resource
61
61
  * validation, and the provider factories receive the metric resource.
62
62
  * Already-created providers keep the resource captured at construction; the
63
- * resource option only sets metrics and tracer instrumentation scope for them.
63
+ * resource option only sets the metric resource for them. Spans and log
64
+ * records carry the instrumentation scope `effect` with the Effect version.
64
65
  * Ownership splits: callers own provider flushing and shutdown, including
65
66
  * factory-built providers, while the layer scope shuts down every reader the
66
67
  * `metricReader` factory returned. No exporter is allocated by this module.
package/src/Otlp.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * `Metric` counters on their hot paths; what they deliberately do not do is
6
6
  * ship an exporter. This module is that exporter: one layer that installs
7
7
  * Effect's own OTLP logger, metrics exporter, and tracer
8
- * (`effect/unstable/observability/Otlp`) against a collector endpoint, with
8
+ * (`effect/observability/Otlp`) against a collector endpoint, with
9
9
  * the flows service identity filled in. Nothing beyond `effect` is involved;
10
10
  * no OpenTelemetry SDK dependency.
11
11
  *
@@ -25,21 +25,21 @@ import * as ConfigProvider from "effect/ConfigProvider"
25
25
  import type * as Duration from "effect/Duration"
26
26
  import * as Effect from "effect/Effect"
27
27
  import * as Exit from "effect/Exit"
28
+ import * as FetchHttpClient from "effect/http/FetchHttpClient"
29
+ import * as Headers from "effect/http/Headers"
30
+ import type * as HttpBody from "effect/http/HttpBody"
31
+ import * as HttpClient from "effect/http/HttpClient"
32
+ import type * as HttpClientRequest from "effect/http/HttpClientRequest"
33
+ import * as HttpClientResponse from "effect/http/HttpClientResponse"
28
34
  import * as Layer from "effect/Layer"
29
35
  import * as Logger from "effect/Logger"
30
36
  import * as Metric from "effect/Metric"
37
+ import * as Otlp from "effect/observability/Otlp"
31
38
  import * as Option from "effect/Option"
32
39
  import * as References from "effect/References"
33
40
  import * as Schema from "effect/Schema"
34
41
  import * as Semaphore from "effect/Semaphore"
35
42
  import * as Tracer from "effect/Tracer"
36
- import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
37
- import * as Headers from "effect/unstable/http/Headers"
38
- import type * as HttpBody from "effect/unstable/http/HttpBody"
39
- import * as HttpClient from "effect/unstable/http/HttpClient"
40
- import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
41
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
42
- import * as Otlp from "effect/unstable/observability/Otlp"
43
43
  import * as Endpoint from "./Endpoint.ts"
44
44
  import * as Resource from "./Resource.ts"
45
45
 
@@ -66,7 +66,7 @@ export const defaultServiceName = "flows"
66
66
  * @category resource
67
67
  * @since 0.1.0
68
68
  */
69
- export const defaultServiceVersion = "1.0.0-rc.5"
69
+ export const defaultServiceVersion = "1.0.0-rc.7"
70
70
 
71
71
  // The upstream logger and tracer each buffer at most one 1,000-record batch.
72
72
  // Do not queue at the transport: upstream forks every full batch independently.
@@ -80,9 +80,11 @@ const maxInFlight = 4
80
80
  export const maxRequestBytes = 1024 * 1024
81
81
  const requestTimeout = "10 seconds"
82
82
  const diagnosticIntervalMillis = 60_000
83
- // The envelope around the resource in every request repeats `service.name` as
84
- // the instrumentation scope name: at most 1,024 code units, at most six bytes
85
- // each once JSON-escaped, so under 8 KiB together with the envelope's own keys.
83
+ // The envelope around the resource in a metrics request repeats `service.name`
84
+ // as the instrumentation scope name: at most 1,024 code units, at most six
85
+ // bytes each once JSON-escaped, so under 8 KiB together with the envelope's own
86
+ // keys. Log and trace requests name their scope `effect` with the Effect
87
+ // version instead, which is smaller.
86
88
  /**
87
89
  * Reserved bytes for request wrappers and the escaped instrumentation scope.
88
90
  * @category transport
@@ -95,8 +97,9 @@ export const maximumEnvelopeBytes = 8 * 1024
95
97
  *
96
98
  * The transport discards a request over 1 MiB, and the resource rides in every
97
99
  * request, so the largest resource `Resource.decode` admits
98
- * (`Resource.maximumResourceBytes`, 128 KiB) and the envelope that repeats the
99
- * service name (under 8 KiB) come off the top. The 888 KiB left gives each
100
+ * (`Resource.maximumResourceBytes`, 128 KiB) and the envelope, which in a
101
+ * metrics request repeats the service name (under 8 KiB), come off the top.
102
+ * The 888 KiB left gives each
100
103
  * record of a full 1,000-record logger or tracer batch about 900 bytes, three
101
104
  * to four times a bare log record or span, and gives the metrics snapshot room
102
105
  * for several thousand series. Every resource the decoder accepts therefore
@@ -111,10 +114,42 @@ export const reservedBatchBytes = maxRequestBytes - Resource.maximumResourceByte
111
114
 
112
115
  type DiscardReason = "oversized" | "stalled" | "saturated"
113
116
 
117
+ /**
118
+ * A collector's answer reduced to what the exporter acts on: its status and
119
+ * its `retry-after` delay, with no body.
120
+ *
121
+ * `Response` refuses a status outside 200 to 599 and a malformed header value.
122
+ * Neither is a collector's answer, so `Response.error()` reports it as status
123
+ * 0, which the exporter fails without retrying. A browser already reports a
124
+ * refused redirect that way.
125
+ */
126
+ const withoutBody = (response: HttpClientResponse.HttpClientResponse): HttpClientResponse.HttpClientResponse => {
127
+ const retryAfter = response.headers["retry-after"]
128
+ let answer: Response
129
+ try {
130
+ answer = new Response(null, {
131
+ status: response.status,
132
+ headers: retryAfter === undefined ? {} : { "retry-after": retryAfter }
133
+ })
134
+ } catch {
135
+ answer = Response.error()
136
+ }
137
+ return HttpClientResponse.fromWeb(response.request, answer)
138
+ }
139
+
114
140
  const boundedClient = Layer.effect(
115
141
  HttpClient.HttpClient,
116
142
  Effect.gen(function*() {
117
- const client = yield* HttpClient.HttpClient
143
+ // The exporter reads every response body to its end, after the deadline
144
+ // and the in-flight limit below have let the request go. A collector that
145
+ // never ended its answer would stall that signal and grow the buffered
146
+ // body without bound. So the exchange ends here: the exporter receives the
147
+ // status and retry delay, and closing the scope aborts the transport with
148
+ // the body unread.
149
+ const client = HttpClient.transformResponse(
150
+ HttpClient.withScope(yield* HttpClient.HttpClient),
151
+ (exchange) => exchange.pipe(Effect.map(withoutBody), Effect.scoped)
152
+ )
118
153
  const clock = yield* Clock
119
154
  // The loggers installed when this transport is acquired. The OTLP logger
120
155
  // the same acquisition installs does not exist yet, so a diagnostic pinned
@@ -329,6 +364,8 @@ export interface Options {
329
364
  * once a minute a `Warn` record with code `otlp_export_discarded` names the
330
365
  * reason, the request size, and the running total through the loggers that
331
366
  * were installed when the layer was acquired, never through this exporter.
367
+ * A collector's response body is never read: the exporter acts on the status
368
+ * and the `retry-after` header, and the exchange ends when they arrive.
332
369
  *
333
370
  * **Details**
334
371
  *
package/src/Resource.ts CHANGED
@@ -8,8 +8,8 @@ import * as OtelResource from "@effect/opentelemetry/Resource"
8
8
  import type { Attributes as OtelAttributes } from "@opentelemetry/api"
9
9
  import * as Effect from "effect/Effect"
10
10
  import * as Layer from "effect/Layer"
11
+ import * as OtlpResource from "effect/observability/OtlpResource"
11
12
  import * as Schema from "effect/Schema"
12
- import * as OtlpResource from "effect/unstable/observability/OtlpResource"
13
13
  import { schemaIssueMessage, schemaIssuePath } from "./internal/schemaIssuePath.ts"
14
14
 
15
15
  /**
@@ -57,11 +57,12 @@ export const maximumAttributes = 256
57
57
  *
58
58
  * The measure is the OTLP `Resource` message the exporters place in every
59
59
  * request, `{"attributes":[{"key":…,"value":{…}}],"droppedAttributesCount":0}`
60
- * with service identity and SDK-added `telemetry.sdk.name` and
61
- * `telemetry.sdk.language` included, after JSON escaping and UTF-8 encoding.
62
- * These SDK fields are conservatively reserved even for the default Effect
63
- * exporter. Caller attributes overwritten by SDK identity or metadata are
64
- * still counted. It is the bound the per-field limits cannot express: 256
60
+ * with service identity and SDK-added `telemetry.sdk.name`,
61
+ * `telemetry.sdk.language` and `telemetry.sdk.version` included, after JSON
62
+ * escaping and UTF-8 encoding. The SDK's own values are reserved even for the
63
+ * default Effect exporter, whose values are shorter. A caller attribute that
64
+ * replaces an SDK field, or that service identity overwrites, is counted
65
+ * beside it. It is the bound the per-field limits cannot express: 256
65
66
  * attributes at the string ceiling would encode to 16 MiB, and the resource
66
67
  * rides in every export request, so the default transport, which discards a
67
68
  * request over 1 MiB, would deliver nothing at all. 128 KiB admits one