add wasm coverage and trace context

This commit is contained in:
Nanaloveyuki
2026-07-19 17:03:57 +08:00
parent 06db039a03
commit f9227890bd
30 changed files with 505 additions and 6 deletions
+10 -2
View File
@@ -67,9 +67,15 @@ jobs:
moon check --deny-warn
moon test --deny-warn
moon check --target native --deny-warn
moon check --target wasm --deny-warn
moon check --target wasm-gc --deny-warn
moon check --target js --deny-warn
- name: Test wasm path
shell: bash
run: |
moon test --target wasm --deny-warn
- name: Run sync example
shell: bash
run: |
@@ -105,15 +111,17 @@ jobs:
run: |
moon update
- name: Check async package on wasm-gc and js
- name: Check async package on wasm, wasm-gc and js
shell: bash
run: |
moon check src-async --target wasm --deny-warn
moon check src-async --target wasm-gc --deny-warn
moon check src-async --target js --deny-warn
- name: Test async package on wasm-gc and js
- name: Test async package on wasm, wasm-gc and js
shell: bash
run: |
moon test src-async --target wasm --deny-warn
moon test src-async --target wasm-gc --deny-warn
moon test src-async --target js --deny-warn
+3 -3
View File
@@ -54,9 +54,8 @@ fn main {
## 支持情况
- `BitLogger` 当前在 CI 中检查/验证的目标是 `native``js``wasm-gc`
- `bitlogger_async` 当前在 CI 中检查 `native``js``wasm-gc`,测试覆盖 `native``js``wasm-gc`
- `wasm` 目标在源码 `moon.pkg` 中保留声明,但当前未纳入 CI 验证口径
- `BitLogger` 当前在 CI 中检查/验证的目标是 `native``wasm``js``wasm-gc`
- `bitlogger_async` 当前在 CI 中检查和测试 `native``wasm``js``wasm-gc`
- `llvm` 目前按实验性目标处理,当前环境未完成验证
- 文件输出是 native 能力;跨端代码里建议先判断 `native_files_supported()`
- `src-async` 可用,但示例 `examples/async_basic` 目前仍按 native 入口提供
@@ -69,6 +68,7 @@ fn main {
- 配置驱动构建:`build_logger(...)``build_async_logger(...)`
- 组合能力:queue、filter、patch、fanout、split、callback
- 异步日志:独立 `src-async` package
- Trace context:标准 `trace_id``span_id``trace_flags``trace_state` 字段绑定
## 文档
+11
View File
@@ -0,0 +1,11 @@
# BitLogger Benchmarks
Run the native baseline from the repository root:
```powershell
moon bench benchmarks --target native --release --deny-warn
```
The suite measures callback emission, trace-context field binding, synchronous queue enqueue-and-flush, and native file writes. It intentionally has no pass/fail threshold: compare results only on the same machine, toolchain, target, and power profile.
Async throughput remains lifecycle-sensitive and is exercised by the native integration test rather than mixed into these synchronous microbenchmarks. Record its end-to-end measurements separately when profiling an application workload.
+61
View File
@@ -0,0 +1,61 @@
///|
let benchmark_record : @log.Record = @log.Record::new(
@log.Level::Info,
"benchmark record",
target="bench",
fields=[@log.field("component", "bitlogger")],
)
///|
test "bench callback logger emission" (it : @bench.T) {
let writes : Ref[Int] = Ref(0)
let logger = @log.Logger::new(@log.callback_sink(fn(_) { writes.val += 1 }))
it.bench(fn() {
logger.info("benchmark record", fields=[
@log.field("component", "bitlogger"),
])
it.keep(writes.val)
})
}
///|
test "bench trace context field binding" (it : @bench.T) {
let writes : Ref[Int] = Ref(0)
let context = @log.trace_context("trace-bench", "span-bench")
let logger = @log.Logger::new(@log.callback_sink(fn(_) { writes.val += 1 })).with_trace_context(
context,
)
it.bench(fn() {
logger.info("benchmark record", fields=[@log.field("event", "request")])
it.keep(writes.val)
})
}
///|
test "bench queued logger enqueue and flush" (it : @bench.T) {
let writes : Ref[Int] = Ref(0)
let logger = @log.Logger::new(@log.callback_sink(fn(_) { writes.val += 1 })).with_queue(
max_pending=8,
)
it.bench(fn() {
logger.info("benchmark record")
it.keep(logger.sink.flush())
})
}
///|
test "bench native file sink write" (it : @bench.T) {
let sink = @log.file_sink(
"logs/bitlogger-benchmark.log",
append=false,
auto_flush=false,
formatter=fn(rec) { rec.message },
)
if sink.is_available() {
it.bench(fn() {
sink.write(benchmark_record)
it.keep(sink.write_failures())
})
ignore(sink.close())
}
}
+4
View File
@@ -0,0 +1,4 @@
import {
"Nanaloveyuki/BitLogger/src" @log,
"moonbitlang/core/bench",
}
+12
View File
@@ -0,0 +1,12 @@
// Generated using `moon info`, DON'T EDIT IT
package "Nanaloveyuki/BitLogger/benchmarks"
// Values
// Errors
// Types and methods
// Type aliases
// Traits
+2
View File
@@ -121,6 +121,7 @@ function buildExtendSidebar(): DefaultTheme.SidebarItem[] {
{ text: 'Sink Composition', link: '/extend/composition' },
{ text: 'Text Formatting', link: '/extend/formatting' },
{ text: 'Target Boundaries', link: '/extend/targets' },
{ text: 'Trace Context', link: '/extend/observability' },
]
}
@@ -141,6 +142,7 @@ function buildChineseExtendSidebar(): DefaultTheme.SidebarItem[] {
{ text: 'Sink 组合', link: '/zh/extend/composition' },
{ text: '文本格式与样式', link: '/zh/extend/formatting' },
{ text: '目标平台边界', link: '/zh/extend/targets' },
{ text: 'Trace Context', link: '/zh/extend/observability' },
]
}
+2 -1
View File
@@ -44,7 +44,7 @@ Continue with the [Examples guide](./examples/index.md): console fields, file ro
## Support Status
- Current local verification covers `native`, `js`, `wasm`, and `wasm-gc` for the main `src` package and `src-async`
- CI checks and tests `native`, `wasm`, `js`, and `wasm-gc` for the main `src` package and `src-async`
- `llvm` is still treated as experimental in the current release context and was not locally re-verified in the current environment
- The source packages still declare `wasm` support alongside `native`, `llvm`, `js`, and `wasm-gc`; see [`target-verification.md`](./api/target-verification.md) for the current release-facing verification boundary
- File output is a native capability; check `native_files_supported()` in cross-target code
@@ -58,6 +58,7 @@ Continue with the [Examples guide](./examples/index.md): console fields, file ro
- Config-based builders: `build_logger(...)` and `build_async_logger(...)`
- Composition helpers: queue, filter, patch, fanout, split, callback
- Separate async package under `src-async`
- Trace-context binding through standard `trace_id`, `span_id`, `trace_flags`, and `trace_state` fields
## Documentation
@@ -0,0 +1,21 @@
---
name: async-logger-with-trace-context
group: api
category: async
update-time: 20260719
description: Bind TraceContext fields before records enter an AsyncLogger queue.
key-word:
- async
- trace
- context
---
## Async-logger-with-trace-context
```moonbit
let contextual = logger.with_trace_context(
@bitlogger.trace_context("trace-1", "span-1"),
)
```
The helper stores the mapped fields on the returned async logger and preserves its queue, lifecycle, target, and threshold state. Context is added before records are enqueued.
+8
View File
@@ -31,6 +31,7 @@ BitLogger API navigation.
- [logger-with-timestamp.md](./logger-with-timestamp.md)
- [logger-with-context-fields.md](./logger-with-context-fields.md)
- [logger-bind.md](./logger-bind.md)
- [logger-with-trace-context.md](./logger-with-trace-context.md)
- [logger-with-filter.md](./logger-with-filter.md)
- [logger-with-patch.md](./logger-with-patch.md)
- [logger-with-queue.md](./logger-with-queue.md)
@@ -99,6 +100,10 @@ BitLogger API navigation.
- [fields.md](./fields.md)
- [text-formatter-config-type.md](./text-formatter-config-type.md)
## Observability
- [trace-context.md](./trace-context.md)
## Record and level
- [record.md](./record.md)
@@ -266,6 +271,7 @@ BitLogger API navigation.
- [async-logger-with-min-level.md](./async-logger-with-min-level.md)
- [async-logger-with-timestamp.md](./async-logger-with-timestamp.md)
- [async-logger-with-context-fields.md](./async-logger-with-context-fields.md)
- [async-logger-with-trace-context.md](./async-logger-with-trace-context.md)
- [async-logger-with-filter.md](./async-logger-with-filter.md)
- [async-logger-with-patch.md](./async-logger-with-patch.md)
@@ -322,6 +328,7 @@ BitLogger API navigation.
- [library-logger-child.md](./library-logger-child.md)
- [library-logger-with-context-fields.md](./library-logger-with-context-fields.md)
- [library-logger-bind.md](./library-logger-bind.md)
- [library-logger-with-trace-context.md](./library-logger-with-trace-context.md)
- [library-logger-is-enabled.md](./library-logger-is-enabled.md)
- [library-logger-log.md](./library-logger-log.md)
- [library-logger-info.md](./library-logger-info.md)
@@ -341,6 +348,7 @@ BitLogger API navigation.
- [library-async-logger-child.md](./library-async-logger-child.md)
- [library-async-logger-with-context-fields.md](./library-async-logger-with-context-fields.md)
- [library-async-logger-bind.md](./library-async-logger-bind.md)
- [library-async-logger-with-trace-context.md](./library-async-logger-with-trace-context.md)
- [library-async-logger-is-enabled.md](./library-async-logger-is-enabled.md)
- [library-async-logger-log.md](./library-async-logger-log.md)
- [library-async-logger-info.md](./library-async-logger-info.md)
@@ -0,0 +1,15 @@
---
name: library-async-logger-with-trace-context
group: api
category: facade
update-time: 20260719
description: Bind TraceContext fields through the restricted LibraryAsyncLogger facade.
key-word:
- library
- async
- trace
---
## Library-async-logger-with-trace-context
`LibraryAsyncLogger::with_trace_context(...)` returns a derived library facade with `trace_id`, `span_id`, `trace_flags`, and optional `trace_state` attached to each later record. It does not start a worker or change lifecycle state.
@@ -0,0 +1,20 @@
---
name: library-logger-with-trace-context
group: api
category: facade
update-time: 20260719
description: Bind TraceContext fields through the restricted LibraryLogger facade.
key-word:
- library
- trace
- context
---
## Library-logger-with-trace-context
```moonbit
let logger = LibraryLogger::new(console_sink())
.with_trace_context(trace_context("trace-1", "span-1"))
```
The derived facade preserves its library-facing API while wrapping the sink with context fields. Use `to_logger()` only when broader composition is required.
+22
View File
@@ -0,0 +1,22 @@
---
name: logger-with-trace-context
group: api
category: logger
update-time: 20260719
description: Bind a TraceContext to every record from a synchronous Logger.
key-word:
- logger
- trace
- context
---
## Logger-with-trace-context
```moonbit
pub fn[S] Logger::with_trace_context(
self : Logger[S],
context : TraceContext,
) -> Logger[ContextSink[S]]
```
Returns a derived logger that prepends the context fields to every record. The original logger is unchanged. This is equivalent to `with_context_fields(context.as_fields())`, while making trace intent explicit.
+25
View File
@@ -0,0 +1,25 @@
---
name: trace-context
group: api
category: observability
update-time: 20260719
description: Represent and bind distributed trace metadata using stable structured-log fields.
key-word:
- trace
- context
- observability
---
## Trace-context
`TraceContext` carries the standard log fields used to correlate records with distributed traces: `trace_id`, `span_id`, `trace_flags`, and optional `trace_state`.
```moonbit
let context = trace_context(
"4bf92f3577b34da6a3ce929d0e0e4736",
"00f067aa0ba902b7",
trace_state="vendor=value",
)
```
Use `context.as_fields()` when integrating with a custom carrier, or pass the value to a logger's `with_trace_context(...)` helper. BitLogger does not parse HTTP headers or export OTLP data; an application-owned tracing library remains responsible for propagation and sampling.
+1
View File
@@ -10,5 +10,6 @@ Start from an [Example flow](../examples/index.md), then add one extension at a
| Send records to multiple destinations or route by level | [Sink composition](./composition.md) | More explicit construction than presets. |
| Control terminal appearance | [Text formatting](./formatting.md) | Formatting affects presentation, not record structure. |
| Share code across native and web targets | [Target boundaries](./targets.md) | File and async runtime behavior differ by backend. |
| Correlate logs with distributed requests | [Trace context](./observability.md) | Propagation and export stay application-owned. |
For exact function signatures, follow each page's links into the [API reference](../api/index.md).
+18
View File
@@ -0,0 +1,18 @@
# Trace Context
BitLogger keeps trace interoperability in its structured record model rather than owning HTTP propagation or a telemetry transport. Use the tracing library already selected by the application to extract a request context, then bind its identifiers to the logger.
```moonbit
let request_logger = logger.with_trace_context(
@log.trace_context(
trace_id,
span_id,
trace_flags="01",
trace_state=trace_state,
),
)
```
Every record from `request_logger` receives `trace_id`, `span_id`, `trace_flags`, and, when supplied, `trace_state`. The base logger is unchanged, so a child request cannot leak identifiers into unrelated work.
Use `context.as_fields()` for a custom logger facade. BitLogger intentionally does not parse `traceparent` headers, create spans, make sampling decisions, or export OTLP data; those concerns remain with the application tracing implementation.
+1
View File
@@ -10,5 +10,6 @@
| 同时发送到多个位置或按 level 路由 | [Sink 组合](./composition.md) | 比 preset 更需要显式构造。 |
| 调整终端可读性 | [文本格式](./formatting.md) | 格式只改变呈现,不改变记录结构。 |
| 在 native 与 web 目标之间共享代码 | [目标平台边界](./targets.md) | 文件与异步运行时行为因后端而异。 |
| 将日志关联到分布式请求 | [Trace context](./observability.md) | 传播与导出仍由应用负责。 |
需要精确函数签名时,沿页面链接进入[英文 API 参考](../../api/index.md)。
+13
View File
@@ -0,0 +1,13 @@
# Trace Context
BitLogger 通过结构化字段承载 trace 上下文,不负责 HTTP 传播或遥测传输。应用应继续使用已选定的 tracing 库提取请求上下文,再把标识绑定到 logger。
```moonbit
let request_logger = logger.with_trace_context(
@log.trace_context(trace_id, span_id, trace_flags="01"),
)
```
派生 logger 的每条记录都会带有 `trace_id``span_id``trace_flags`,以及传入时的 `trace_state`。原 logger 不会被修改,避免请求上下文泄漏到无关任务。
`context.as_fields()` 可用于自定义 facade。`traceparent` 解析、span 创建、采样与 OTLP 导出仍由应用选用的 tracing 实现负责。
+24
View File
@@ -455,6 +455,30 @@ async test "async logger with_context_fields prepends shared fields without muta
inspect(written_fields.val[2].value, content="test")
}
///|
test "async facades bind trace context through standard fields" {
let context = @bitlogger.trace_context("trace-async", "span-async")
let base = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
)
let derived = base.with_trace_context(context)
inspect(base.context_fields.length(), content="0")
inspect(derived.context_fields.length(), content="3")
inspect(derived.context_fields[0].key, content="trace_id")
inspect(derived.context_fields[0].value, content="trace-async")
inspect(derived.context_fields[1].key, content="span_id")
inspect(derived.context_fields[2].value, content="01")
let library = LibraryAsyncLogger::new(@bitlogger.callback_sink(fn(_) { })).with_trace_context(
context,
)
let full = library.to_async_logger()
inspect(full.context_fields.length(), content="3")
inspect(full.context_fields[1].value, content="span-async")
}
///|
async test "async logger child composes target and preserves other state" {
let written_target : Ref[String] = Ref("")
+8
View File
@@ -274,6 +274,14 @@ pub fn[S] AsyncLogger::with_context_fields(
{ ..self, context_fields: fields }
}
///|
pub fn[S] AsyncLogger::with_trace_context(
self : AsyncLogger[S],
context : @bitlogger.TraceContext,
) -> AsyncLogger[S] {
self.with_context_fields(context.as_fields())
}
///|
pub fn[S] AsyncLogger::with_filter(
self : AsyncLogger[S],
@@ -0,0 +1,90 @@
///|
async test "native async file lifecycle flushes rotates and restarts" {
if !@bitlogger.native_files_supported() {
inspect(@bitlogger.native_files_supported(), content="false")
} else {
let path = "logs/bitlogger-async-native-e2e.log"
let first_flushes : Ref[Int] = Ref(0)
let first_sink = @bitlogger.file_sink(
path,
append=false,
auto_flush=false,
rotation=Some(@bitlogger.file_rotation(8, max_backups=1)),
formatter=fn(rec) { rec.message },
)
let first = async_logger(
first_sink,
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=4,
linger_ms=0,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.file.e2e",
flush=fn(sink) {
first_flushes.val += 1
ignore(sink.flush())
},
)
@async.with_task_group(group => {
group.spawn_bg(() => first.run())
first.info("first-record")
first.shutdown()
})
inspect(first.is_closed(), content="true")
inspect(first.has_failed(), content="false")
inspect(first.pending_count(), content="0")
inspect(first_flushes.val > 0, content="true")
inspect(first_sink.path(), content="logs/bitlogger-async-native-e2e.log")
inspect(first_sink.rotation_enabled(), content="true")
inspect(first_sink.rotation_failures(), content="0")
inspect(first_sink.write_failures(), content="0")
inspect(first_sink.flush_failures(), content="0")
inspect(first_sink.close(), content="true")
let restart_flushes : Ref[Int] = Ref(0)
let restarted_sink = @bitlogger.file_sink(
path,
append=true,
auto_flush=false,
rotation=Some(@bitlogger.file_rotation(8, max_backups=1)),
formatter=fn(rec) { rec.message },
)
let restarted = async_logger(
restarted_sink,
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=4,
linger_ms=0,
flush=AsyncFlushPolicy::Shutdown,
),
min_level=@bitlogger.Level::Info,
target="async.file.e2e",
flush=fn(sink) {
restart_flushes.val += 1
ignore(sink.flush())
},
)
@async.with_task_group(group => {
group.spawn_bg(() => restarted.run())
restarted.info("second-record")
restarted.shutdown()
})
inspect(restarted.is_closed(), content="true")
inspect(restarted.has_failed(), content="false")
inspect(restarted.pending_count(), content="0")
inspect(restart_flushes.val, content="1")
inspect(restarted_sink.rotation_enabled(), content="true")
inspect(restarted_sink.rotation_failures(), content="0")
inspect(restarted_sink.write_failures(), content="0")
inspect(restarted_sink.flush_failures(), content="0")
inspect(restarted_sink.close(), content="true")
}
}
+8
View File
@@ -93,6 +93,14 @@ pub fn[S] LibraryAsyncLogger::bind(
self.with_context_fields(fields)
}
///|
pub fn[S] LibraryAsyncLogger::with_trace_context(
self : LibraryAsyncLogger[S],
context : @bitlogger.TraceContext,
) -> LibraryAsyncLogger[S] {
library_async_logger(self.inner.with_trace_context(context))
}
///|
pub fn[S] LibraryAsyncLogger::is_enabled(
self : LibraryAsyncLogger[S],
+2
View File
@@ -114,6 +114,7 @@ pub fn[S] AsyncLogger::with_min_level(Self[S], @core.Level) -> Self[S]
pub fn[S] AsyncLogger::with_patch(Self[S], (@core.Record) -> @core.Record) -> Self[S]
pub fn[S] AsyncLogger::with_target(Self[S], String) -> Self[S]
pub fn[S] AsyncLogger::with_timestamp(Self[S], enabled? : Bool) -> Self[S]
pub fn[S] AsyncLogger::with_trace_context(Self[S], @core.TraceContext) -> Self[S]
pub struct LibraryAsyncLogger[S] {
inner : AsyncLogger[S]
@@ -131,6 +132,7 @@ pub fn[S] LibraryAsyncLogger::to_async_logger(Self[S]) -> AsyncLogger[S]
pub async fn[S] LibraryAsyncLogger::warn(Self[S], String, fields? : Array[@core.Field]) -> Unit
pub fn[S] LibraryAsyncLogger::with_context_fields(Self[S], Array[@core.Field]) -> Self[S]
pub fn[S] LibraryAsyncLogger::with_target(Self[S], String) -> Self[S]
pub fn[S] LibraryAsyncLogger::with_trace_context(Self[S], @core.TraceContext) -> Self[S]
// Type aliases
pub type ApplicationAsyncLogger = AsyncLogger[@runtime.RuntimeSink]
+9
View File
@@ -39,6 +39,15 @@ pub fn Record::with_fields(Self, Array[Field]) -> Self
pub fn Record::with_message(Self, String) -> Self
pub fn Record::with_target(Self, String) -> Self
pub struct TraceContext {
trace_id : String
span_id : String
trace_flags : String
trace_state : String?
}
pub fn TraceContext::as_fields(Self) -> Array[Field]
pub fn TraceContext::new(String, String, trace_flags? : String, trace_state? : String) -> Self
// Type aliases
// Traits
+31
View File
@@ -0,0 +1,31 @@
///|
pub struct TraceContext {
trace_id : String
span_id : String
trace_flags : String
trace_state : String?
}
///|
pub fn TraceContext::new(
trace_id : String,
span_id : String,
trace_flags? : String = "01",
trace_state? : String,
) -> TraceContext {
{ trace_id, span_id, trace_flags, trace_state }
}
///|
pub fn TraceContext::as_fields(self : TraceContext) -> Array[Field] {
let fields = [
field("trace_id", self.trace_id),
field("span_id", self.span_id),
field("trace_flags", self.trace_flags),
]
match self.trace_state {
Some(value) => fields.push(field("trace_state", value))
None => ()
}
fields
}
+8
View File
@@ -80,6 +80,14 @@ pub fn[S] LibraryLogger::bind(
self.with_context_fields(fields)
}
///|
pub fn[S] LibraryLogger::with_trace_context(
self : LibraryLogger[S],
context : TraceContext,
) -> LibraryLogger[ContextSink[S]] {
library_logger(self.inner.with_trace_context(context))
}
///|
pub fn[S] LibraryLogger::is_enabled(
self : LibraryLogger[S],
+8
View File
@@ -57,6 +57,14 @@ pub fn[S] Logger::bind(
self.with_context_fields(fields)
}
///|
pub fn[S] Logger::with_trace_context(
self : Logger[S],
context : TraceContext,
) -> Logger[ContextSink[S]] {
self.with_context_fields(context.as_fields())
}
///|
pub fn[S] Logger::with_filter(
self : Logger[S],
+6
View File
@@ -187,6 +187,8 @@ pub fn text_style(fg? : String?, bg? : String?, bold? : Bool, dim? : Bool, itali
pub fn trace(String, fields? : Array[@core.Field]) -> Unit
pub fn trace_context(String, String, trace_flags? : String, trace_state? : String) -> @core.TraceContext
pub fn warn(String, fields? : Array[@core.Field]) -> Unit
pub fn with_file_rotation(@config_model.LoggerConfig, Int, max_backups? : Int) -> @config_model.LoggerConfig
@@ -212,6 +214,7 @@ pub fn[S] LibraryLogger::to_logger(Self[S]) -> Logger[S]
pub fn[S : @sink_graph.Sink] LibraryLogger::warn(Self[S], String, fields? : Array[@core.Field]) -> Unit
pub fn[S] LibraryLogger::with_context_fields(Self[S], Array[@core.Field]) -> Self[@sink_graph.ContextSink[S]]
pub fn[S] LibraryLogger::with_target(Self[S], String) -> Self[S]
pub fn[S] LibraryLogger::with_trace_context(Self[S], @core.TraceContext) -> Self[@sink_graph.ContextSink[S]]
pub struct Logger[S] {
min_level : @core.Level
@@ -276,6 +279,7 @@ pub fn[S] Logger::with_patch(Self[S], (@core.Record) -> @core.Record) -> Self[@s
pub fn[S] Logger::with_queue(Self[S], max_pending? : Int, overflow? : @queue_model.QueueOverflowPolicy) -> Self[@sink_graph.QueuedSink[S]]
pub fn[S] Logger::with_target(Self[S], String) -> Self[S]
pub fn[S] Logger::with_timestamp(Self[S], enabled? : Bool) -> Self[S]
pub fn[S] Logger::with_trace_context(Self[S], @core.TraceContext) -> Self[@sink_graph.ContextSink[S]]
// Type aliases
pub type ApplicationLogger = Logger[@runtime.RuntimeSink]
@@ -358,6 +362,8 @@ pub using @config_model {type TextFormatterConfig}
pub using @formatting {type TextStyle}
pub using @core {type TraceContext}
pub using @sink_graph {trait Sink}
// Traits
+22
View File
@@ -1,6 +1,9 @@
///|
pub type Field = @core.Field
///|
pub type TraceContext = @core.TraceContext
///|
pub fn field(key : String, value : String) -> Field {
@core.field(key, value)
@@ -11,6 +14,25 @@ pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
@core.fields(entries)
}
///|
pub fn trace_context(
trace_id : String,
span_id : String,
trace_flags? : String = "01",
trace_state? : String,
) -> TraceContext {
match trace_state {
Some(value) =>
@core.TraceContext::new(
trace_id,
span_id,
trace_flags~,
trace_state=value,
)
None => @core.TraceContext::new(trace_id, span_id, trace_flags~)
}
}
///|
pub type Record = @core.Record
+40
View File
@@ -0,0 +1,40 @@
///|
test "trace context maps W3C fields in a stable order" {
let context = trace_context(
"4bf92f3577b34da6a3ce929d0e0e4736",
"00f067aa0ba902b7",
trace_state="vendor=value",
)
let fields = context.as_fields()
inspect(fields.length(), content="4")
inspect(fields[0].key, content="trace_id")
inspect(fields[0].value, content="4bf92f3577b34da6a3ce929d0e0e4736")
inspect(fields[1].key, content="span_id")
inspect(fields[1].value, content="00f067aa0ba902b7")
inspect(fields[2].key, content="trace_flags")
inspect(fields[2].value, content="01")
inspect(fields[3].key, content="trace_state")
inspect(fields[3].value, content="vendor=value")
}
///|
test "sync facades bind trace context without mutating their base logger" {
let captured : Ref[Array[Field]] = Ref([])
let base = Logger::new(callback_sink(fn(rec) { captured.val = rec.fields }))
let context = trace_context("trace-1", "span-1", trace_flags="00")
let derived = base.with_trace_context(context)
base.info("base")
inspect(captured.val.length(), content="0")
derived.info("derived", fields=[field("event", "request")])
inspect(captured.val.length(), content="4")
inspect(captured.val[0].key, content="trace_id")
inspect(captured.val[2].value, content="00")
inspect(captured.val[3].key, content="event")
let library = LibraryLogger::new(
callback_sink(fn(rec) { captured.val = rec.fields }),
).with_trace_context(context)
library.info("library")
inspect(captured.val.length(), content="3")
inspect(captured.val[1].value, content="span-1")
}