152 lines
5.4 KiB
TypeScript
152 lines
5.4 KiB
TypeScript
import { assert, describe, it } from "@effect/vitest"
|
|
import { Array as Arr, ConfigProvider, Context, Effect, Layer, Metric, Ref } from "effect"
|
|
import { TestClock } from "effect/testing"
|
|
import { HttpClient, type HttpClientError, HttpClientResponse } from "effect/unstable/http"
|
|
import { Otlp, OtlpSerialization } from "effect/unstable/observability"
|
|
|
|
describe("Otlp.layerFromConfig", () => {
|
|
it.effect("configures exporters from OTEL environment variables", () =>
|
|
Effect.gen(function*() {
|
|
yield* Effect.log("configured log")
|
|
yield* Metric.update(Metric.counter("otel_config_counter"), 1)
|
|
yield* Effect.void.pipe(Effect.withSpan("configured-span"))
|
|
|
|
yield* TestClock.adjust("1 second")
|
|
|
|
const requests = yield* MockHttpClient.requests
|
|
const logRequest = findRequest(requests, "http://logs.example/custom")
|
|
const metricRequest = findRequest(requests, "http://collector.example/v1/metrics")
|
|
const traceRequest = findRequest(requests, "http://collector.example/v1/traces")
|
|
|
|
assert.isDefined(logRequest)
|
|
assert.strictEqual(logRequest!.headers["x-signal"], "logs")
|
|
assert.strictEqual(logRequest!.headers["x-shared"], undefined)
|
|
|
|
assert.isDefined(metricRequest)
|
|
assert.strictEqual(metricRequest!.headers["x-shared"], "shared")
|
|
const metric = findMetric(metricRequest!.body as OtlpMetricsBody, "otel_config_counter")
|
|
assert.strictEqual(metric?.sum?.aggregationTemporality, 1)
|
|
|
|
assert.isDefined(traceRequest)
|
|
assert.strictEqual(traceRequest!.headers["x-shared"], "shared")
|
|
}).pipe(
|
|
Effect.provide(testLayer({
|
|
OTEL_SERVICE_NAME: "otel-config-test",
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector.example",
|
|
OTEL_EXPORTER_OTLP_HEADERS: "x-shared=shared",
|
|
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://logs.example/custom",
|
|
OTEL_EXPORTER_OTLP_LOGS_HEADERS: "x-signal=logs",
|
|
OTEL_LOGS_EXPORTER: "console, OTLP",
|
|
OTEL_METRICS_EXPORTER: " otlp ",
|
|
OTEL_TRACES_EXPORTER: "otlp",
|
|
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: "1",
|
|
OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "1",
|
|
OTEL_METRIC_EXPORT_INTERVAL: "1000",
|
|
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "delta"
|
|
}))
|
|
))
|
|
|
|
it.effect("does not install exporters when OTEL_SDK_DISABLED is true", () =>
|
|
Effect.gen(function*() {
|
|
yield* Effect.log("disabled log")
|
|
yield* Metric.update(Metric.counter("otel_disabled_counter"), 1)
|
|
yield* Effect.void.pipe(Effect.withSpan("disabled-span"))
|
|
|
|
yield* TestClock.adjust("1 second")
|
|
|
|
const requests = yield* MockHttpClient.requests
|
|
assert.deepStrictEqual(requests, [])
|
|
}).pipe(
|
|
Effect.provide(testLayer({
|
|
OTEL_SDK_DISABLED: "true",
|
|
OTEL_SERVICE_NAME: "otel-config-test",
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector.example",
|
|
OTEL_LOGS_EXPORTER: "otlp",
|
|
OTEL_METRICS_EXPORTER: "otlp",
|
|
OTEL_TRACES_EXPORTER: "otlp",
|
|
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: "1",
|
|
OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "1",
|
|
OTEL_METRIC_EXPORT_INTERVAL: "1000"
|
|
}))
|
|
))
|
|
})
|
|
|
|
interface CapturedRequest {
|
|
readonly url: string
|
|
readonly headers: Record<string, string>
|
|
readonly body: unknown
|
|
}
|
|
|
|
interface OtlpMetricsBody {
|
|
readonly resourceMetrics: Array<{
|
|
readonly scopeMetrics: Array<{
|
|
readonly metrics: Array<OtlpMetric>
|
|
}>
|
|
}>
|
|
}
|
|
|
|
interface OtlpMetric {
|
|
readonly name: string
|
|
readonly sum?: {
|
|
readonly aggregationTemporality: number
|
|
} | undefined
|
|
}
|
|
|
|
class MockHttpClient extends Context.Service<MockHttpClient, {
|
|
readonly requests: Effect.Effect<ReadonlyArray<CapturedRequest>>
|
|
}>()("MockHttpClient") {
|
|
static requests = Effect.service(MockHttpClient).pipe(
|
|
Effect.flatMap((client) => client.requests)
|
|
)
|
|
}
|
|
|
|
const testLayer = (env: Record<string, string>) =>
|
|
Otlp.layerFromConfig({ loggerMergeWithExisting: false }).pipe(
|
|
Layer.provide(OtlpSerialization.layerJson),
|
|
Layer.provideMerge(HttpClientLayer),
|
|
Layer.provideMerge(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))
|
|
)
|
|
|
|
const HttpClientLayer = Layer.effectContext(Effect.gen(function*() {
|
|
const capturedRequests = yield* Ref.make<ReadonlyArray<CapturedRequest>>([])
|
|
|
|
const httpClient = HttpClient.makeWith(
|
|
Effect.fnUntraced(function*(requestEffect) {
|
|
const request = yield* requestEffect
|
|
const body = request.body._tag === "Uint8Array"
|
|
? JSON.parse(new TextDecoder().decode(request.body.body))
|
|
: undefined
|
|
yield* Ref.update(
|
|
capturedRequests,
|
|
Arr.append({
|
|
url: request.url,
|
|
headers: request.headers,
|
|
body
|
|
})
|
|
)
|
|
return HttpClientResponse.fromWeb(request, new Response())
|
|
}),
|
|
Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
|
|
)
|
|
|
|
return Context.make(HttpClient.HttpClient, httpClient).pipe(
|
|
Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) }))
|
|
)
|
|
}))
|
|
|
|
const findRequest = (requests: ReadonlyArray<CapturedRequest>, url: string) =>
|
|
requests.find((request) => request.url === url)
|
|
|
|
const findMetric = (request: OtlpMetricsBody, name: string): OtlpMetric | undefined => {
|
|
for (const resourceMetrics of request.resourceMetrics) {
|
|
for (const scopeMetrics of resourceMetrics.scopeMetrics) {
|
|
for (const metric of scopeMetrics.metrics) {
|
|
if (metric.name === name) {
|
|
return metric
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return undefined
|
|
}
|