Skip to content

Commit

Permalink
Add support for W3C Trace Context propagation (#2445)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
vecerek and tim-smart authored Apr 2, 2024
1 parent 63a1df2 commit 5170ce7
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 135 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-wombats-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/platform": patch
---

Add support for W3C Trace Context propagation
5 changes: 5 additions & 0 deletions .changeset/great-insects-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

generate proper trace ids in default effect Tracer
7 changes: 4 additions & 3 deletions packages/effect/src/internal/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const tracerTag = Context.GenericTag<Tracer.Tracer>("effect/Tracer")
/** @internal */
export const spanTag = Context.GenericTag<Tracer.ParentSpan>("effect/ParentSpan")

const randomString = (function() {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
const randomHexString = (function() {
const characters = "abcdef0123456789"
const charactersLength = characters.length
return function(length: number) {
let result = ""
Expand Down Expand Up @@ -56,7 +56,8 @@ export class NativeSpan implements Tracer.Span {
startTime
}
this.attributes = new Map()
this.spanId = `span${randomString(16)}`
this.traceId = parent._tag === "Some" ? parent.value.traceId : randomHexString(32)
this.spanId = randomHexString(16)
}

end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
Expand Down
73 changes: 2 additions & 71 deletions packages/platform/src/Http/IncomingMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
* @since 1.0.0
*/
import type { ParseOptions } from "@effect/schema/AST"
import * as ParseResult from "@effect/schema/ParseResult"
import type * as ParseResult from "@effect/schema/ParseResult"
import * as Schema from "@effect/schema/Schema"
import * as Effect from "effect/Effect"
import * as FiberRef from "effect/FiberRef"
import { dual, flow } from "effect/Function"
import { dual } from "effect/Function"
import * as Global from "effect/GlobalValue"
import type { Inspectable } from "effect/Inspectable"
import * as Option from "effect/Option"
import type * as Stream from "effect/Stream"
import * as Tracer from "effect/Tracer"
import type { ExternalSpan } from "effect/Tracer"
import * as FileSystem from "../FileSystem.js"
import type * as Headers from "./Headers.js"
import type * as UrlParams from "./UrlParams.js"
Expand Down Expand Up @@ -112,73 +110,6 @@ export const schemaHeadersEffect = <R, I extends Readonly<Record<string, string>
return <E, E2, R2>(effect: Effect.Effect<IncomingMessage<E>, E2, R2>) => Effect.scoped(Effect.flatMap(effect, decode))
}

const SpanSchema = Schema.struct({
traceId: Schema.string,
spanId: Schema.string,
parentSpanId: Schema.union(Schema.string, Schema.undefined),
sampled: Schema.boolean
})

/**
* @since 1.0.0
* @category schema
*/
export const schemaExternalSpan: <E>(
self: IncomingMessage<E>
) => Effect.Effect<Tracer.ExternalSpan, ParseResult.ParseError> = flow(
schemaHeaders(Schema.union(
Schema.transformOrFail(
Schema.struct({
b3: Schema.NonEmpty
}),
SpanSchema,
(input, _, ast) => {
const parts = input.b3.split("-")
if (parts.length >= 2) {
return ParseResult.succeed(
{
traceId: parts[0],
spanId: parts[1],
sampled: parts[2] ? parts[2] === "1" : true,
parentSpanId: parts[3]
} as const
)
}
return ParseResult.fail(new ParseResult.Type(ast, input))
},
(_) => ParseResult.succeed({ b3: "" } as const)
),
Schema.transform(
Schema.struct({
"x-b3-traceid": Schema.NonEmpty,
"x-b3-spanid": Schema.NonEmpty,
"x-b3-parentspanid": Schema.optional(Schema.NonEmpty),
"x-b3-sampled": Schema.optional(Schema.NonEmpty, { default: () => "1" })
}),
SpanSchema,
(_) => ({
traceId: _["x-b3-traceid"],
spanId: _["x-b3-spanid"],
parentSpanId: _["x-b3-parentspanid"],
sampled: _["x-b3-sampled"] === "1"
} as const),
(_) => ({
"x-b3-traceid": _.traceId,
"x-b3-spanid": _.spanId,
"x-b3-parentspanid": _.parentSpanId,
"x-b3-sampled": _.sampled ? "1" : "0"
} as const)
)
)),
Effect.map((_): ExternalSpan =>
Tracer.externalSpan({
traceId: _.traceId,
spanId: _.spanId,
sampled: _.sampled
})
)
)

/**
* @since 1.0.0
* @category fiber refs
Expand Down
109 changes: 109 additions & 0 deletions packages/platform/src/Http/TraceContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @since 1.0.0
*/
import * as Option from "effect/Option"
import * as Tracer from "effect/Tracer"
import * as Headers from "./Headers.js"

/**
* @since 1.0.0
* @category models
*/
export interface FromHeaders {
(headers: Headers.Headers): Option.Option<Tracer.ExternalSpan>
}

/**
* @since 1.0.0
* @category encoding
*/
export const toHeaders = (span: Tracer.Span): Headers.Headers =>
Headers.unsafeFromRecord({
b3: `${span.traceId}-${span.spanId}-${span.sampled ? "1" : "0"}${
span.parent._tag === "Some" ? `-${span.parent.value.spanId}` : ""
}`,
traceparent: `00-${span.traceId}-${span.spanId}-${span.sampled ? "01" : "00"}`
})

/**
* @since 1.0.0
* @category decoding
*/
export const fromHeaders = (headers: Headers.Headers): Option.Option<Tracer.ExternalSpan> => {
let span = w3c(headers)
if (span._tag === "Some") {
return span
}
span = b3(headers)
if (span._tag === "Some") {
return span
}
return xb3(headers)
}

/**
* @since 1.0.0
* @category decoding
*/
export const b3: FromHeaders = (headers) => {
if (!("b3" in headers)) {
return Option.none()
}
const parts = headers["b3"].split("-")
if (parts.length < 2) {
return Option.none()
}
return Option.some(Tracer.externalSpan({
traceId: parts[0],
spanId: parts[1],
sampled: parts[2] ? parts[2] === "1" : true
}))
}

/**
* @since 1.0.0
* @category decoding
*/
export const xb3: FromHeaders = (headers) => {
if (!(headers["x-b3-traceid"]) || !(headers["x-b3-spanid"])) {
return Option.none()
}
return Option.some(Tracer.externalSpan({
traceId: headers["x-b3-traceid"],
spanId: headers["x-b3-spanid"],
sampled: headers["x-b3-sampled"] ? headers["x-b3-sampled"] === "1" : true
}))
}

const w3cTraceId = /^[0-9a-f]{32}$/gi
const w3cSpanId = /^[0-9a-f]{16}$/gi

/**
* @since 1.0.0
* @category decoding
*/
export const w3c: FromHeaders = (headers) => {
if (!(headers["traceparent"])) {
return Option.none()
}
const parts = headers["traceparent"].split("-")
if (parts.length !== 4) {
return Option.none()
}
const [version, traceId, spanId, flags] = parts
switch (version) {
case "00": {
if (w3cTraceId.test(traceId) === false || w3cSpanId.test(spanId) === false) {
return Option.none()
}
return Option.some(Tracer.externalSpan({
traceId,
spanId,
sampled: (parseInt(flags, 16) & 1) === 1
}))
}
default: {
return Option.none()
}
}
}
17 changes: 6 additions & 11 deletions packages/platform/src/internal/http/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import * as Ref from "effect/Ref"
import type * as Schedule from "effect/Schedule"
import type * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import type * as Tracer from "effect/Tracer"
import type * as Body from "../../Http/Body.js"
import type * as Client from "../../Http/Client.js"
import * as Error from "../../Http/ClientError.js"
import type * as ClientRequest from "../../Http/ClientRequest.js"
import type * as ClientResponse from "../../Http/ClientResponse.js"
import * as Cookies from "../../Http/Cookies.js"
import * as Method from "../../Http/Method.js"
import * as TraceContext from "../../Http/TraceContext.js"
import * as UrlParams from "../../Http/UrlParams.js"
import * as internalBody from "./body.js"
import * as internalRequest from "./clientRequest.js"
Expand Down Expand Up @@ -77,15 +77,6 @@ export const make = <R, E, A, R2, E2>(
return client as any
}

const addB3Header = (req: ClientRequest.ClientRequest, span: Tracer.Span) =>
internalRequest.setHeader(
req,
"b3",
`${span.traceId}-${span.spanId}-${span.sampled ? "1" : "0"}${
span.parent._tag === "Some" ? `-${span.parent.value.spanId}` : ""
}`
)

/** @internal */
export const makeDefault = (
f: (
Expand All @@ -109,7 +100,11 @@ export const makeDefault = (
"http.url": request.url
}
},
(span) => Effect.withParentSpan(f(addB3Header(request, span), fiber), span)
(span) =>
Effect.withParentSpan(
f(internalRequest.setHeaders(request, TraceContext.toHeaders(span)), fiber),
span
)
)
})),
Effect.succeed as Client.Client.Preprocess<never, never>
Expand Down
Loading

0 comments on commit 5170ce7

Please sign in to comment.