Created
September 15, 2025 08:19
-
-
Save afonsomatos/1fb4b4d4976d8d05256f7da4bd30ed36 to your computer and use it in GitHub Desktop.
Datadog Effect Logger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Array, | |
Cause, | |
Context, | |
Effect, | |
Either, | |
FiberRef, | |
FiberRefs, | |
flow, | |
HashMap, | |
HashSet, | |
Inspectable, | |
Layer, | |
Logger, | |
Option, | |
Struct, | |
Tracer | |
} from "effect"; | |
/** | |
* Format the traceId and spanId to be compatible with Datadog. | |
* | |
* Converts the OTEL traceId (128-bit uint as 32-hex-char string) and spanId (64-bit uint as 16-hex-char string) | |
* to the Datadog traceId and spanId (64-bit uint) format. | |
*/ | |
const withDatadogFormat = (span: Tracer.AnySpan): Tracer.AnySpan => { | |
const { spanId, traceId } = span; | |
const traceIdEnd = traceId.slice(traceId.length / 2); | |
return Struct.evolve(span, { | |
traceId: () => BigInt(`0x${traceIdEnd}`).toString(), | |
spanId: () => BigInt(`0x${spanId}`).toString() | |
}); | |
}; | |
/** | |
* `Effect.fn` adds a virtual span, so we have to grab it from the outside. | |
*/ | |
const filterSpan = (span: Option.Option<Tracer.AnySpan>): Option.Option<Tracer.AnySpan> => { | |
if (span._tag === "Some") { | |
if (span.value._tag === "Span" && span.value.name === "<anonymous>") { | |
return filterSpan(span.value.parent); | |
} else { | |
return span; | |
} | |
} | |
return Option.none(); | |
}; | |
/** | |
* Add identifiers from the current Span to the log annotations. | |
*/ | |
const withDatadogSpanAnnotations = <Message, Output>( | |
self: Logger.Logger<Message, Output> | |
): Logger.Logger<Message, Output> => | |
Logger.mapInputOptions(self, (options: Logger.Logger.Options<Message>) => { | |
const span = filterSpan( | |
Context.getOption(FiberRefs.getOrDefault(options.context, FiberRef.currentContext), Tracer.ParentSpan) | |
); | |
if (span._tag === "None") { | |
return options; | |
} | |
const { spanId, traceId } = withDatadogFormat(span.value); | |
return Struct.evolve(options, { | |
annotations: flow( | |
HashMap.set("dd.trace_id", traceId as unknown), | |
HashMap.set("dd.span_id", spanId as unknown) | |
) | |
}); | |
}); | |
/** | |
* A logger compatible with Datadog format. Objects are used for custom attributes. | |
*/ | |
const DatadogLogger = Logger.make(({ logLevel, annotations, cause, message, date }) => { | |
const [messages, attributes] = Array.partitionMap(Array.ensure(message), (message) => { | |
if (Array.isArray(message)) { | |
return Either.left(message.join(" ")); | |
} else if (typeof message === "object" && message !== null) { | |
return Either.right(message); | |
} else { | |
return Either.left(`${message}`); | |
} | |
}); | |
const objAnnotations = Object.fromEntries(HashMap.entries(annotations)); | |
return Inspectable.stringifyCircular({ | |
level: logLevel.label, | |
timestamp: date.toISOString(), | |
// Usually it's just one | |
message: messages.join(" ").trim(), | |
// Some logs will be empty but have some errors, for example: Effect.tapErrorCause(Effect.logWarning) | |
cause: Cause.isEmpty(cause) ? undefined : Cause.pretty(cause, { renderErrorCause: true }), | |
// These include the datadog's tracing values | |
...objAnnotations, | |
// Assume that plain objects are to be custom attributes. | |
...Object.assign({}, ...attributes) | |
}); | |
}).pipe(withDatadogSpanAnnotations, Logger.withConsoleLog); | |
export const LoggerLive = Layer.scopedDiscard( | |
Effect.locallyScoped(FiberRef.currentLoggers, HashSet.make(DatadogLogger)) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment