@llm4ts/runner 2.29.0 → 2.30.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.
- package/dist/Doctor.d.ts +4 -1
- package/dist/Doctor.d.ts.map +1 -1
- package/dist/Doctor.js +51 -44
- package/dist/Doctor.js.map +1 -1
- package/dist/FlowArgs.d.ts +1 -1
- package/dist/FlowArgs.d.ts.map +1 -1
- package/dist/FlowArgs.js +2 -0
- package/dist/FlowArgs.js.map +1 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +40 -7
- package/dist/FlowRunner.js.map +1 -1
- package/dist/Otel.d.ts +57 -0
- package/dist/Otel.d.ts.map +1 -0
- package/dist/Otel.js +140 -0
- package/dist/Otel.js.map +1 -0
- package/dist/Package.d.ts +1 -0
- package/dist/Package.d.ts.map +1 -1
- package/dist/Package.js +5 -0
- package/dist/Package.js.map +1 -1
- package/package.json +4 -3
- package/src/Doctor.ts +67 -56
- package/src/FlowArgs.ts +2 -0
- package/src/FlowRunner.ts +73 -8
- package/src/Otel.ts +198 -0
- package/src/Package.ts +10 -0
package/src/Otel.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// OpenTelemetry export (ADR 0026): Effect's own OTLP tracer and metrics
|
|
2
|
+
// exporters over core's fetch client, so a run can show up in Phoenix,
|
|
3
|
+
// Langfuse, SigNoz or any collector with no SDK and no new dependency.
|
|
4
|
+
// The standard OTEL_* variables are the truth; `--otel` is the laptop case.
|
|
5
|
+
import * as ConfigProvider from "effect/ConfigProvider"
|
|
6
|
+
import * as Duration from "effect/Duration"
|
|
7
|
+
import * as Effect from "effect/Effect"
|
|
8
|
+
import * as Layer from "effect/Layer"
|
|
9
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient"
|
|
10
|
+
import * as OtlpExporter from "effect/observability/OtlpExporter"
|
|
11
|
+
import * as OtlpMetrics from "effect/observability/OtlpMetrics"
|
|
12
|
+
import * as OtlpSerialization from "effect/observability/OtlpSerialization"
|
|
13
|
+
import * as OtlpTracer from "effect/observability/OtlpTracer"
|
|
14
|
+
import { packageVersion } from "./Package.ts"
|
|
15
|
+
|
|
16
|
+
export type OtelConfig =
|
|
17
|
+
| { readonly mode: "off" }
|
|
18
|
+
| { readonly mode: "phoenix"; readonly tracesUrl: string }
|
|
19
|
+
| { readonly mode: "env"; readonly endpoint: string }
|
|
20
|
+
|
|
21
|
+
export const phoenixTracesUrl = "http://localhost:6006/v1/traces"
|
|
22
|
+
|
|
23
|
+
const truthy = (value: string | undefined): boolean =>
|
|
24
|
+
/^(on|1|true|yes)$/iu.test(value?.trim() ?? "")
|
|
25
|
+
const falsy = (value: string | undefined): boolean =>
|
|
26
|
+
/^(off|0|false|no)$/iu.test(value?.trim() ?? "")
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Off unless asked. An OTLP endpoint in the environment wins and is used as
|
|
30
|
+
* the standard variables say; `LLM4TS_OTEL=on` alone means a local Phoenix,
|
|
31
|
+
* traces only. `OTEL_SDK_DISABLED` or `LLM4TS_OTEL=off` switch everything off.
|
|
32
|
+
*/
|
|
33
|
+
export const otelConfig = (
|
|
34
|
+
environment: Readonly<Record<string, string | undefined>>
|
|
35
|
+
): OtelConfig => {
|
|
36
|
+
if (truthy(environment.OTEL_SDK_DISABLED) || falsy(environment.LLM4TS_OTEL)) {
|
|
37
|
+
return { mode: "off" }
|
|
38
|
+
}
|
|
39
|
+
// Under the test runner the exporter's sleeps run on a TestClock nobody
|
|
40
|
+
// advances, so its final flush would wait forever: a developer with
|
|
41
|
+
// LLM4TS_OTEL=on in the shell must still get a green suite.
|
|
42
|
+
if (environment.VITEST !== undefined) {
|
|
43
|
+
return { mode: "off" }
|
|
44
|
+
}
|
|
45
|
+
const endpoint =
|
|
46
|
+
environment.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim() ||
|
|
47
|
+
environment.OTEL_EXPORTER_OTLP_ENDPOINT?.trim()
|
|
48
|
+
if (endpoint !== undefined && endpoint.length > 0) {
|
|
49
|
+
return { mode: "env", endpoint }
|
|
50
|
+
}
|
|
51
|
+
return truthy(environment.LLM4TS_OTEL)
|
|
52
|
+
? { mode: "phoenix", tracesUrl: phoenixTracesUrl }
|
|
53
|
+
: { mode: "off" }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OtelResource {
|
|
57
|
+
readonly serviceVersion: string
|
|
58
|
+
/** Phoenix's project: the repository's basename, never a path. */
|
|
59
|
+
readonly project: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The run end may wait this long for the last batch; the event drain waits the same. */
|
|
63
|
+
export const flushTimeout = Duration.seconds(3)
|
|
64
|
+
|
|
65
|
+
/** This runner's version, for `service.version`. */
|
|
66
|
+
export const runnerVersion: string = packageVersion
|
|
67
|
+
|
|
68
|
+
const resourceOf = (resource: OtelResource) => ({
|
|
69
|
+
serviceName: "llm4ts",
|
|
70
|
+
serviceVersion: resource.serviceVersion,
|
|
71
|
+
attributes: { "openinference.project.name": resource.project }
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The exporter for `config`: nothing when off; traces only for Phoenix;
|
|
76
|
+
* traces and metrics from the standard variables for a generic endpoint.
|
|
77
|
+
* Logs are never exported. Export errors are the exporter's to swallow; the
|
|
78
|
+
* run never fails on them.
|
|
79
|
+
*/
|
|
80
|
+
export const otelLayer = (
|
|
81
|
+
config: OtelConfig,
|
|
82
|
+
resource: OtelResource,
|
|
83
|
+
/** What the standard variables are read from in env mode (the run's environment). */
|
|
84
|
+
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
85
|
+
): Layer.Layer<OtlpExporter.Flusher> => {
|
|
86
|
+
switch (config.mode) {
|
|
87
|
+
case "off":
|
|
88
|
+
// The exporter's no-op flusher: nothing to export, nothing to flush.
|
|
89
|
+
return OtlpExporter.layerFlusher
|
|
90
|
+
case "phoenix":
|
|
91
|
+
return OtlpTracer.layer({
|
|
92
|
+
url: config.tracesUrl,
|
|
93
|
+
resource: resourceOf(resource),
|
|
94
|
+
shutdownTimeout: flushTimeout
|
|
95
|
+
}).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
|
|
96
|
+
case "env":
|
|
97
|
+
// The standard variables are read from the run's own environment, with
|
|
98
|
+
// the two exporter defaults an endpoint implies; the process env is
|
|
99
|
+
// never written.
|
|
100
|
+
return Layer.mergeAll(
|
|
101
|
+
OtlpTracer.layerFromConfig({ resource: resourceOf(resource) }),
|
|
102
|
+
OtlpMetrics.layerFromConfig({ resource: resourceOf(resource) })
|
|
103
|
+
).pipe(
|
|
104
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
105
|
+
Layer.provide(FetchHttpClient.layer),
|
|
106
|
+
Layer.provide(
|
|
107
|
+
ConfigProvider.layer(
|
|
108
|
+
ConfigProvider.fromEnvRecord({
|
|
109
|
+
...environment,
|
|
110
|
+
...otelEnvironmentDefaults(config, environment)
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `layerFromConfig` exports nothing unless `OTEL_TRACES_EXPORTER` names
|
|
120
|
+
* `otlp`; an endpoint in the environment is the operator's intent, so the
|
|
121
|
+
* layer reads its variables with that default (and the metrics one) added
|
|
122
|
+
* when they are unset.
|
|
123
|
+
*/
|
|
124
|
+
export const otelEnvironmentDefaults = (
|
|
125
|
+
config: OtelConfig,
|
|
126
|
+
environment: Readonly<Record<string, string | undefined>>
|
|
127
|
+
): Readonly<Record<string, string>> =>
|
|
128
|
+
config.mode !== "env"
|
|
129
|
+
? {}
|
|
130
|
+
: {
|
|
131
|
+
...(environment.OTEL_TRACES_EXPORTER === undefined ? { OTEL_TRACES_EXPORTER: "otlp" } : {}),
|
|
132
|
+
...(environment.OTEL_METRICS_EXPORTER === undefined
|
|
133
|
+
? { OTEL_METRICS_EXPORTER: "otlp" }
|
|
134
|
+
: {})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const otelSummary = (config: OtelConfig): string | undefined => {
|
|
138
|
+
switch (config.mode) {
|
|
139
|
+
case "off":
|
|
140
|
+
return undefined
|
|
141
|
+
case "phoenix":
|
|
142
|
+
return `otel → Phoenix at ${config.tracesUrl} (traces)`
|
|
143
|
+
case "env":
|
|
144
|
+
return `otel → ${config.endpoint} (traces + metrics, OTEL_* variables)`
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The one warning a run prints when export is on and nothing answers at the
|
|
150
|
+
* endpoint (spec §9): the exporter itself only logs at debug level and drops
|
|
151
|
+
* the batch. Nothing when off or when the endpoint answered.
|
|
152
|
+
*/
|
|
153
|
+
export const otelWarning = (
|
|
154
|
+
config: OtelConfig,
|
|
155
|
+
answers: boolean | undefined
|
|
156
|
+
): string | undefined => {
|
|
157
|
+
if (config.mode === "off" || answers !== false) {
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
const where = config.mode === "phoenix" ? config.tracesUrl : config.endpoint
|
|
161
|
+
return (
|
|
162
|
+
`⚠ otel: nothing answers at ${where} — spans will be dropped (the run is unaffected)` +
|
|
163
|
+
(config.mode === "phoenix"
|
|
164
|
+
? "; start Phoenix with `docker run -p 6006:6006 arizephoenix/phoenix:latest`"
|
|
165
|
+
: "")
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The doctor's line: `answers` is the probe's result, undefined when export is off. */
|
|
170
|
+
export const otelDoctorLine = (config: OtelConfig, answers: boolean | undefined): string => {
|
|
171
|
+
switch (config.mode) {
|
|
172
|
+
case "off":
|
|
173
|
+
return "otel: off (LLM4TS_OTEL=on for a local Phoenix, or OTEL_EXPORTER_OTLP_ENDPOINT)"
|
|
174
|
+
case "phoenix":
|
|
175
|
+
case "env": {
|
|
176
|
+
const where = config.mode === "phoenix" ? `Phoenix at ${config.tracesUrl}` : config.endpoint
|
|
177
|
+
return `otel: ${where} — ${
|
|
178
|
+
answers === true
|
|
179
|
+
? "endpoint answers"
|
|
180
|
+
: "endpoint does not answer (spans will be dropped, the run is unaffected)"
|
|
181
|
+
}`
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Whether anything listens at the endpoint: any HTTP answer counts, a refused connection does not. */
|
|
187
|
+
export const probeOtelEndpoint = (config: OtelConfig): Effect.Effect<boolean | undefined> =>
|
|
188
|
+
config.mode === "off"
|
|
189
|
+
? Effect.succeed(undefined)
|
|
190
|
+
: Effect.tryPromise(() =>
|
|
191
|
+
fetch(config.mode === "phoenix" ? config.tracesUrl : config.endpoint, {
|
|
192
|
+
method: "OPTIONS",
|
|
193
|
+
signal: AbortSignal.timeout(1_500)
|
|
194
|
+
})
|
|
195
|
+
).pipe(
|
|
196
|
+
Effect.as(true),
|
|
197
|
+
Effect.orElseSucceed(() => false)
|
|
198
|
+
)
|
package/src/Package.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
|
|
4
|
+
const Manifest = Schema.Struct({ version: Schema.String })
|
|
5
|
+
|
|
6
|
+
const manifest = Schema.decodeUnknownSync(Manifest)(
|
|
7
|
+
createRequire(import.meta.url)("../package.json")
|
|
8
|
+
)
|
|
9
|
+
|
|
1
10
|
export const packageName = "@llm4ts/runner"
|
|
11
|
+
export const packageVersion: string = manifest.version
|