Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'

This commit is contained in:
-Puter
2026-07-19 03:25:10 +05:30
parent dd1071cc10
commit 2daf979036
2214 changed files with 673090 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
import * as NodeSdk from "@effect/opentelemetry/NodeSdk"
import { assert, describe, it } from "@effect/vitest"
import { SeverityNumber } from "@opentelemetry/api-logs"
import { InMemoryLogRecordExporter, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs"
import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
import * as Clock from "effect/Clock"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as References from "effect/References"
describe("Logger", () => {
describe("provided", () => {
const exporter = new InMemoryLogRecordExporter()
const TracingLive = NodeSdk.layer(Effect.sync(() => ({
resource: {
serviceName: "test"
},
logRecordProcessor: [new SimpleLogRecordProcessor({ exporter })]
})))
it.effect("emits log records", () =>
Effect.gen(function*() {
yield* Effect.log("test").pipe(
Effect.repeat({ times: 9 })
)
assert.lengthOf(exporter.getFinishedLogRecords(), 10)
}).pipe(Effect.provide(TracingLive)))
it.effect("maps Effect LogLevel to OTel SeverityNumber spec values", () => {
const severityExporter = new InMemoryLogRecordExporter()
const SeverityLayer = NodeSdk.layer(Effect.sync(() => ({
resource: { serviceName: "test" },
logRecordProcessor: [new SimpleLogRecordProcessor({ exporter: severityExporter })]
})))
return Effect.gen(function*() {
yield* Effect.logTrace("trace")
yield* Effect.logDebug("debug")
yield* Effect.logInfo("info")
yield* Effect.logWarning("warn")
yield* Effect.logError("error")
yield* Effect.logFatal("fatal")
const records = severityExporter.getFinishedLogRecords()
const byText = Object.fromEntries(records.map((r) => [r.severityText, r.severityNumber]))
assert.strictEqual(byText.Trace, SeverityNumber.TRACE)
assert.strictEqual(byText.Debug, SeverityNumber.DEBUG)
assert.strictEqual(byText.Info, SeverityNumber.INFO)
assert.strictEqual(byText.Warn, SeverityNumber.WARN)
assert.strictEqual(byText.Error, SeverityNumber.ERROR)
assert.strictEqual(byText.Fatal, SeverityNumber.FATAL)
}).pipe(
Effect.provide(SeverityLayer.pipe(Layer.provideMerge(Layer.succeed(References.MinimumLogLevel, "Trace"))))
)
})
it.effect("uses monotonic clock timestamps and keeps them aligned with spans", () => {
const logExporter = new InMemoryLogRecordExporter()
const spanExporter = new InMemorySpanExporter()
const timeNanos = 1_735_689_600_123_456_789n
const expectedTime: readonly [number, number] = [
Number(timeNanos / BigInt(1_000_000_000)),
Number(timeNanos % BigInt(1_000_000_000))
]
const skewedClock: Clock.Clock = {
currentTimeMillisUnsafe: () => 1,
currentTimeMillis: Effect.succeed(1),
currentTimeNanosUnsafe: () => timeNanos,
currentTimeNanos: Effect.succeed(timeNanos),
sleep: () => Effect.void
}
const TracingLive = NodeSdk.layer(Effect.sync(() => ({
resource: {
serviceName: "test"
},
spanProcessor: [new SimpleSpanProcessor(spanExporter)],
logRecordProcessor: [new SimpleLogRecordProcessor({ exporter: logExporter })]
})))
return Effect.gen(function*() {
yield* Effect.log("test").pipe(
Effect.withSpan("parent")
)
const logs = logExporter.getFinishedLogRecords()
const spans = spanExporter.getFinishedSpans()
assert.lengthOf(logs, 1)
assert.lengthOf(spans, 1)
const log = logs[0]!
const span = spans[0]!
assert.deepStrictEqual(log.hrTime, expectedTime)
assert.deepStrictEqual(log.hrTimeObserved, expectedTime)
assert.deepStrictEqual(log.hrTime, span.startTime)
assert.strictEqual(log.attributes.spanId, span.spanContext().spanId)
assert.strictEqual(log.attributes.traceId, span.spanContext().traceId)
}).pipe(
Effect.provide(TracingLive),
Effect.provideService(Clock.Clock, skewedClock)
)
})
})
describe("not provided", () => {
const exporter = new InMemoryLogRecordExporter()
const TracingLive = NodeSdk.layer(Effect.sync(() => ({
resource: {
serviceName: "test"
}
})))
it.effect("withSpan", () =>
Effect.gen(function*() {
yield* Effect.log("test")
assert.lengthOf(exporter.getFinishedLogRecords(), 0)
}).pipe(Effect.provide(TracingLive)))
})
})

View File

@@ -0,0 +1,339 @@
import * as internal from "@effect/opentelemetry/internal/metrics"
import { assert, describe, it } from "@effect/vitest"
import { ValueType } from "@opentelemetry/api"
import { resourceFromAttributes } from "@opentelemetry/resources"
import * as Effect from "effect/Effect"
import * as Metric from "effect/Metric"
const findMetric = (metrics: any, name: string) =>
metrics.resourceMetrics.scopeMetrics[0].metrics.find((_: any) => _.descriptor.name === name)
describe("Metrics", () => {
it.effect("gauge", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const resource = resourceFromAttributes({
name: "test",
version: "1.0.0"
})
const producer = new internal.MetricProducerImpl(resource, services)
const gauge = Metric.gauge("rps")
yield* Metric.withAttributes(gauge, {
key: "value",
unit: "requests"
}).pipe(Metric.update(10))
yield* Metric.withAttributes(gauge, {
key: "value"
}).pipe(Metric.update(10))
yield* Metric.withAttributes(gauge, {
key: "value"
}).pipe(Metric.update(20))
const results = yield* Effect.promise(() => producer.collect())
const object = JSON.parse(JSON.stringify(results))
assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
["name", "test"],
["version", "1.0.0"]
])
assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
const metric = findMetric(object, "rps")
assert.deepStrictEqual(metric, {
"dataPointType": 2,
"descriptor": {
"advice": {},
"name": "rps",
"description": "",
"unit": "requests",
"type": "OBSERVABLE_GAUGE",
"valueType": ValueType.DOUBLE
},
"aggregationTemporality": 1,
"dataPoints": [
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"unit": "requests",
"key": "value"
},
"value": 10
},
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"key": "value"
},
"value": 20
}
]
})
}))
it.effect("gauge bigint", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const producer = new internal.MetricProducerImpl(
resourceFromAttributes({
name: "test",
version: "1.0.0"
}),
services
)
const gauge = Metric.gauge("rps-bigint", { bigint: true })
yield* Metric.withAttributes(gauge, {
key: "value",
unit: "requests"
}).pipe(Metric.update(10n))
yield* Metric.withAttributes(gauge, {
key: "value"
}).pipe(Metric.update(10n))
yield* Metric.withAttributes(gauge, {
key: "value"
}).pipe(Metric.update(20n))
const results = yield* Effect.promise(() => producer.collect())
const object = JSON.parse(JSON.stringify(results))
assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
["name", "test"],
["version", "1.0.0"]
])
assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
const metric = findMetric(object, "rps-bigint")
assert.deepEqual(metric, {
"dataPointType": 2,
"descriptor": {
"advice": {},
"name": "rps-bigint",
"description": "",
"unit": "requests",
"type": "OBSERVABLE_GAUGE",
"valueType": ValueType.INT
},
"aggregationTemporality": 1,
"dataPoints": [
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"unit": "requests",
"key": "value"
},
"value": 10
},
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"key": "value"
},
"value": 20
}
]
})
}))
it.effect("counter", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const producer = new internal.MetricProducerImpl(
resourceFromAttributes({
name: "test",
version: "1.0.0"
}),
services
)
const counter = Metric.counter("counter", { description: "Example" })
yield* Metric.withAttributes(counter, {
key: "value",
unit: "requests"
}).pipe(Metric.update(1))
yield* Metric.withAttributes(counter, {
key: "value"
}).pipe(Metric.update(1))
yield* Metric.withAttributes(counter, {
key: "value"
}).pipe(Metric.update(1))
const results = yield* Effect.promise(() => producer.collect())
const object = JSON.parse(JSON.stringify(results))
assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
["name", "test"],
["version", "1.0.0"]
])
assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
const metric = findMetric(object, "counter")
assert.deepEqual(metric, {
"dataPointType": 3,
"descriptor": {
"advice": {},
"name": "counter",
"description": "Example",
"unit": "requests",
"type": "UP_DOWN_COUNTER",
"valueType": ValueType.DOUBLE
},
"isMonotonic": false,
"aggregationTemporality": 1,
"dataPoints": [
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"unit": "requests",
"key": "value"
},
"value": 1
},
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"key": "value"
},
"value": 2
}
]
})
}))
it.effect("counter-inc", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const producer = new internal.MetricProducerImpl(
resourceFromAttributes({
name: "test",
version: "1.0.0"
}),
services
)
const counter = Metric.counter("counter-inc", {
description: "Example",
incremental: true
})
yield* Metric.withAttributes(counter, {
key: "value",
unit: "requests"
}).pipe(Metric.update(1))
yield* Metric.withAttributes(counter, {
key: "value"
}).pipe(Metric.update(1))
yield* Metric.withAttributes(counter, {
key: "value"
}).pipe(Metric.update(1))
const results = yield* Effect.promise(() => producer.collect())
const object = JSON.parse(JSON.stringify(results))
assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
["name", "test"],
["version", "1.0.0"]
])
assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
const metric = findMetric(object, "counter-inc")
assert.deepEqual(metric, {
"dataPointType": 3,
"descriptor": {
"advice": {},
"name": "counter-inc",
"description": "Example",
"unit": "requests",
"type": "COUNTER",
"valueType": ValueType.DOUBLE
},
"isMonotonic": true,
"aggregationTemporality": 1,
"dataPoints": [
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"unit": "requests",
"key": "value"
},
"value": 1
},
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"key": "value"
},
"value": 2
}
]
})
}))
it.effect("counter-bigint", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const producer = new internal.MetricProducerImpl(
resourceFromAttributes({
name: "test",
version: "1.0.0"
}),
services
)
const counter = Metric.counter("counter-bigint", {
description: "Example",
incremental: true,
bigint: true
})
yield* Metric.withAttributes(counter, {
key: "value",
unit: "requests"
}).pipe(Metric.update(1n))
yield* Metric.withAttributes(counter, {
key: "value"
}).pipe(Metric.update(1n))
yield* Metric.withAttributes(counter, {
key: "value"
}).pipe(Metric.update(1n))
const results = yield* Effect.promise(() => producer.collect())
const object = JSON.parse(JSON.stringify(results))
assert.deepEqual(object.resourceMetrics.resource._rawAttributes, [
["name", "test"],
["version", "1.0.0"]
])
assert.equal(object.resourceMetrics.scopeMetrics.length, 1)
const metric = findMetric(object, "counter-bigint")
assert.deepEqual(metric, {
"dataPointType": 3,
"descriptor": {
"advice": {},
"name": "counter-bigint",
"description": "Example",
"unit": "requests",
"type": "COUNTER",
"valueType": ValueType.INT
},
"isMonotonic": true,
"aggregationTemporality": 1,
"dataPoints": [
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"unit": "requests",
"key": "value"
},
"value": 1
},
{
"startTime": metric.dataPoints[0].startTime,
"endTime": metric.dataPoints[0].endTime,
"attributes": {
"key": "value"
},
"value": 2
}
]
})
}))
})

View File

@@ -0,0 +1,212 @@
import * as NodeSdk from "@effect/opentelemetry/NodeSdk"
import * as OtelTracer from "@effect/opentelemetry/OtelTracer"
import { assert, describe, it } from "@effect/vitest"
import * as OtelApi from "@opentelemetry/api"
import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks"
import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
import * as Cause from "effect/Cause"
import * as Effect from "effect/Effect"
import * as EffectTracer from "effect/Tracer"
const TracingLive = NodeSdk.layer(Effect.sync(() => ({
resource: {
serviceName: "test"
},
spanProcessor: [new SimpleSpanProcessor(new InMemorySpanExporter())]
})))
// needed to test context propagation
const contextManager = new AsyncHooksContextManager()
OtelApi.context.setGlobalContextManager(contextManager)
describe("Tracer", () => {
describe("provided", () => {
it.effect("withSpan", () =>
Effect.gen(function*() {
const span = yield* Effect.currentSpan
assert.instanceOf(span, OtelTracer.OtelSpan)
}).pipe(
Effect.withSpan("ok"),
Effect.provide(TracingLive)
))
it.effect("withSpan links", () =>
Effect.gen(function*() {
const linkedSpan = yield* Effect.makeSpanScoped("B")
const span = yield* Effect.currentSpan.pipe(
Effect.withSpan("A"),
Effect.linkSpans(linkedSpan)
)
assert.instanceOf(span, OtelTracer.OtelSpan)
assert.lengthOf(span.links, 1)
}).pipe(
Effect.scoped,
Effect.provide(TracingLive)
))
it.effect("nested withSpan sets correct parent chain", () =>
Effect.gen(function*() {
const child = yield* Effect.currentSpan.pipe(
Effect.withSpan("child"),
Effect.withSpan("parent")
)
assert.instanceOf(child, OtelTracer.OtelSpan)
assert.strictEqual(child.name, "child")
assert.isDefined(child.parent)
assert.strictEqual((child.parent.valueOrUndefined! as OtelTracer.OtelSpan).name, "parent")
}).pipe(
Effect.provide(TracingLive)
))
it.effect("supervisor sets context", () =>
Effect.sync(() => {
const context = OtelApi.context.active()
assert.isDefined(OtelApi.trace.getSpan(context))
}).pipe(
Effect.withSpan("ok"),
Effect.provide(TracingLive)
))
it.effect("supervisor sets context generator", () =>
Effect.gen(function*() {
yield* Effect.yieldNow
const context = OtelApi.context.active()
assert.isDefined(OtelApi.trace.getSpan(context))
}).pipe(
Effect.withSpan("ok"),
Effect.provide(TracingLive)
))
it.effect("currentOtelSpan", () =>
Effect.gen(function*() {
const span = yield* Effect.currentSpan
const otelSpan = yield* OtelTracer.currentOtelSpan
assert.strictEqual((span as OtelTracer.OtelSpan).span, otelSpan)
}).pipe(
Effect.withSpan("ok"),
Effect.provide(TracingLive)
))
it.effect("preserves the sampling decision of generic external spans", () =>
Effect.gen(function*() {
const span = yield* Effect.currentSpan
assert.instanceOf(span, OtelTracer.OtelSpan)
assert.strictEqual(span.span.spanContext().traceFlags, OtelApi.TraceFlags.NONE)
}).pipe(
Effect.withSpan("child"),
Effect.withParentSpan(EffectTracer.externalSpan({
traceId: "1".repeat(32),
spanId: "2".repeat(16),
sampled: false
})),
Effect.provide(TracingLive)
))
it.effect("records every pretty error", () =>
Effect.gen(function*() {
const exporter = new InMemorySpanExporter()
const spanProcessor = new SimpleSpanProcessor(exporter)
const firstFailure = Cause.fail(new Error("first"))
const secondFailure = Cause.fail(new Error("second"))
const cause = Cause.combine(firstFailure, secondFailure)
yield* Effect.failCause(cause).pipe(
Effect.withSpan("error-span"),
Effect.andThen(Effect.never), // keep the exporter alive
Effect.provide(NodeSdk.layer(() => ({
resource: {
serviceName: "test"
},
spanProcessor: [spanProcessor]
}))),
Effect.forkChild({ startImmediately: true })
)
const spanData = exporter.getFinishedSpans()[0]
if (spanData === undefined) {
return yield* Effect.die("Missing span data")
}
const exceptionEvents = spanData.events.filter((event) => event.name === "exception")
assert.lengthOf(exceptionEvents, 2)
assert.strictEqual(spanData.status.message, "first")
}))
it.effect("renders nested error causes in the stacktrace", () =>
Effect.gen(function*() {
const exporter = new InMemorySpanExporter()
const spanProcessor = new SimpleSpanProcessor(exporter)
const error = new Error("outer failure", { cause: new Error("inner cause") })
yield* Effect.die(error).pipe(
Effect.withSpan("error-span"),
Effect.andThen(Effect.never), // keep the exporter alive
Effect.provide(NodeSdk.layer(() => ({
resource: {
serviceName: "test"
},
spanProcessor: [spanProcessor]
}))),
Effect.forkChild({ startImmediately: true })
)
const spanData = exporter.getFinishedSpans()[0]
if (spanData === undefined) {
return yield* Effect.die("Missing span data")
}
const exceptionEvent = spanData.events.find((event) => event.name === "exception")
assert(exceptionEvent !== undefined)
const stacktrace = exceptionEvent.attributes?.["exception.stacktrace"]
assert.isString(stacktrace)
assert.include(stacktrace as string, "[cause]: Error: inner cause")
}))
it.effect("withSpanContext", () =>
Effect.gen(function*() {
const effect = Effect.gen(function*() {
const span = yield* Effect.currentParentSpan
assert(span._tag === "Span")
if (span.parent._tag === "None") {
return yield* Effect.die("No parent span")
}
return span.parent.value
}).pipe(Effect.withSpan("child"))
const services = yield* Effect.context<never>()
yield* Effect.promise(async () => {
await OtelApi.trace.getTracer("test").startActiveSpan("otel-span", {
root: true,
attributes: { "root": "yes" }
}, async (span) => {
try {
const parent = await effect.pipe(
OtelTracer.withSpanContext(span.spanContext()),
Effect.provideContext(services),
Effect.runPromise
)
const { spanId, traceId } = span.spanContext()
assert.containsSubset(parent, {
spanId,
traceId
})
} finally {
span.end()
}
})
})
}).pipe(
Effect.scoped,
Effect.provide(TracingLive)
))
})
describe("not provided", () => {
it.effect("withSpan", () =>
Effect.gen(function*() {
const span = yield* Effect.currentSpan
assert.notInstanceOf(span, OtelTracer.OtelSpan)
}).pipe(
Effect.withSpan("ok")
))
})
})