agent-telemetry

Download zip
Version
0.1.6
License
MulanPSL-2.0
Last updated
last month
Downloads
86

#agent-telemetry

OpenTelemetry instrumentation library for MoonBit Agent / LLM / Tool scenarios.

It wraps the boilerplate OTel tracer / span / attribute code into business-semantic helpers, so agent developers only need to decide when to create spans and what business data to record.

#Design Layers

  • Thin wrapper (lib.mbt): provider initialization, tracer cache, span lifecycle helpers such as start_span / end_span.
  • Semantic wrapper (genai.mbt, tool.mbt, agent.mbt): sets attributes according to the OpenTelemetry GenAI semantic conventions, covering chat, tool execution, and agent turn spans.

#Quick Start

moon add cybershang/agent-telemetry

// Option 1: one-line initialization from environment variables.
// Reads OTEL_SERVICE_NAME, OTEL_STDOUT, and OTEL_EXPORTER_OTLP_ENDPOINT.
// Use ProcessUniqueRandom to avoid duplicate trace/span IDs across restarts.
let providers = @telemetry.init_from_env(
id_generator=@telemetry.ProcessUniqueRandom,
)

// Option 2: manually select an exporter.
let config = @telemetry.TelemetryConfig::new(service_name="my-agent")
let providers = @telemetry.init_telemetry(
config,
@telemetry.Otlp("http://localhost:4318"),
id_generator=@telemetry.ProcessUniqueRandom,
)

// Metrics and logs need background tasks.
@async.with_task_group((group) => {
providers.spawn_background_tasks(group)

// Create a GenAI chat span.
let tracer = @telemetry.tracer("my-agent/llm")
let meter = @telemetry.meter("my-agent/llm")
let span = @telemetry.start_chat_span(
tracer,
"stepfun",
"step-3.7-flash",
1024,
temperature=Some(0.7),
stream=Some(true),
)
// ... send the request and obtain response_json ...
@telemetry.set_usage(span, 100L, 25L, cache_read_input_tokens=50L)
@telemetry.set_response(span, response_json, output_messages~)
@telemetry.end_span(span)

providers.force_flush()
providers.shutdown()
})

#Main API

ScenarioFunctions
GeneralTelemetryConfig::new, init_telemetry, init_from_env, TelemetryProviders, IdGeneratorOption, tracer, meter, logger, conversation_logger, start_span, end_span, set_attributes, set_string, set_int, set_double, set_bool, set_json
LLM chatstart_chat_span, set_usage, set_response, set_http_error
Toolstart_tool_span, set_tool_result, set_tool_error
Agent turnstart_agent_turn_span, set_turn, set_turn_exhausted
Agent invocationstart_invoke_agent_span
Planstart_plan_span
Metricsrecord_llm_latency, record_usage, record_tool_call, record_turn, record_agent_duration, record_tool_duration
Logsemit_log, log_info, log_warn, log_error, log_conversation_message

#Custom Span Attributes

The library already sets the standard GenAI semantic attributes for chat/tool/agent spans. When you need to record your own domain-specific metadata, use the typed attribute helpers:

let span = @telemetry.start_span(tracer, "my.step")
@telemetry.set_string(span, "app.user.id", "user-42")
@telemetry.set_int(span, "app.retry.count", 3L)
@telemetry.set_double(span, "app.score", 0.95)
@telemetry.set_bool(span, "app.cached", true)
@telemetry.set_json(span, "app.metadata", { "source": "api", "depth": 2 })
@telemetry.end_span(span)

set_json serializes the Json value to a string, which is useful for structured metadata that does not fit the scalar attribute types. For arbitrary attribute lists you can still use set_attributes(span, attrs) with @otel.KeyValue values.

#Instrumentation Guide

See docs/instrumentation.md for a step-by-step guide that shows how to instrument LLM chat requests, tool executions, and agent turns using the agent-observability sample application as a reference.

#Environment Variables

init_from_env reads the following variables:

VariableDescriptionDefault
OTEL_SERVICE_NAMEService nameagent-telemetry
OTEL_STDOUTUse the stdout exporter when set to truefalse
OTEL_EXPORTER_OTLP_ENDPOINTOTLP/HTTP endpointhttp://localhost:4318
OTEL_EXPORTER_OTLP_HEADERSGlobal OTLP headers, comma-separated key=value pairs(empty)
OTEL_EXPORTER_OTLP_TRACES_HEADERSTrace-specific OTLP headers(empty, falls back to global)
OTEL_EXPORTER_OTLP_METRICS_HEADERSMetric-specific OTLP headers(empty, falls back to global)
OTEL_EXPORTER_OTLP_LOGS_HEADERSLog-specific OTLP headers(empty, falls back to global)
GREPTIME_TRACE_PIPELINEGreptimeDB trace pipeline namegreptime_trace_v1
GREPTIME_LOG_TABLETarget table for conversation logsgenai_conversations

Signal-specific header variables take precedence over OTEL_EXPORTER_OTLP_HEADERS. This is useful for backends that require authentication tokens or custom routing headers.

For example:

OTEL_EXPORTER_OTLP_LOGS_HEADERS="Authorization=Bearer my-token, X-Scope-OrgID=tenant-1"

#Log Routing

The library maintains two separate loggers:

  • Default logger (logger / log_info / log_warn / log_error) writes to the standard OTLP logs table (opentelemetry_logs by default).
  • Conversation logger (conversation_logger / log_conversation_message) writes to the table configured by GREPTIME_LOG_TABLE (default genai_conversations).

When exporting to GreptimeDB, this lets you keep general application logs and LLM conversation logs in separate tables.

#Flushing and Shutting Down

TelemetryProviders exposes force_flush() and shutdown() methods that flush/shutdown all three providers (traces, metrics, logs) and print any errors to stdout.

providers.force_flush()
providers.shutdown()

Always call shutdown before the process exits so pending telemetry is exported.

#ID Generator Option

The id_generator parameter of init_telemetry / init_from_env supports:

OptionDescription
@telemetry.SdkDefaultUse the SDK default RandomIdGenerator
@telemetry.ProcessUniqueRandomGenerate a process-unique seed from the current timestamp and a counter, avoiding duplicate trace/span IDs across restarts
@telemetry.Custom(generator)Provide your own IdGenerator

#Target Backend

This library depends on opentelemetry/otlp, whose async/http and async/socket interfaces are native-only. Therefore moon.mod declares preferred_target = "native". Projects using this library should also run and test with --target native.

#Example Project

A complete agent example using this library can be found at:

#Testing

moon test -p cybershang/agent-telemetry --target native

#License

Mulan PSL v2

ExporterType

pub(all) enum ExporterType {
Stdout
Otlp(String)
Custom(
SpanExporter
)
NoOp
}

Exporter type for telemetry initialization.

IdGeneratorOption

pub(all) enum IdGeneratorOption {
SdkDefault
ProcessUniqueRandom
Custom(
IdGenerator
)
}

ID generator option for telemetry initialization.

TelemetryConfig

pub struct TelemetryConfig {
service_name : String
} derive(
Debug
)

Configuration for initializing OpenTelemetry in an agent application.

TelemetryConfig::new

fn TelemetryConfig::new(service_name? : String) -> TelemetryConfig

Create a new telemetry configuration.

TelemetryProviders

Holder for all initialized OpenTelemetry SDK providers.

TelemetryProviders::force_flush

async fn TelemetryProviders::force_flush(self : TelemetryProviders) -> Unit

Flush all providers and print any errors.

TelemetryProviders::shutdown

async fn TelemetryProviders::shutdown(self : TelemetryProviders) -> Unit

Shut down all providers and print any errors.

TelemetryProviders::spawn_background_tasks

fn TelemetryProviders::spawn_background_tasks(self : TelemetryProviders, group :
TaskGroup
[Unit], allow_failure? : Bool) -> Unit

Spawn background tasks required by metrics (periodic reader) and logs (batch processor).

conversation_logger

fn conversation_logger(scope_name : String) ->
Logger

Get or create a logger for conversation messages. Logs from this logger go to the configured GenAI conversations table (default genai_conversations).

emit_log

Emit a structured log record.

end_span

End a span, optionally setting a final status.

init_from_env

async fn init_from_env(service_name? : String, id_generator? : IdGeneratorOption) -> TelemetryProviders

Initialize the global providers from standard environment variables.

Reads:
  • OTEL_SERVICE_NAME (overrides service_name)
  • OTEL_STDOUT (true selects the stdout exporter)
  • OTEL_EXPORTER_OTLP_ENDPOINT (when OTEL_STDOUT is not true)

init_telemetry

async fn init_telemetry(config : TelemetryConfig, exporter_type : ExporterType, id_generator? : IdGeneratorOption) -> TelemetryProviders

Initialize the global OpenTelemetry providers (traces, metrics, logs).

log_conversation_message

async fn log_conversation_message(scope_name : String, role : String, content : String, index? : Int, trace_context? :
SpanContext
?) -> Unit

Emit a conversation message log compatible with the genai-observability GreptimeDB dashboard SQL.

Body JSON shape:
  • user / tool: {"content":"..."}
  • assistant: {"index":0,"message":{"role":"assistant","content":"..."}}

log_error

async fn log_error(scope_name : String, body : String, attributes? : Array[
KeyValue
], trace_context? :
SpanContext
?) -> Unit

Emit an error log.

log_info

async fn log_info(scope_name : String, body : String, attributes? : Array[
KeyValue
], trace_context? :
SpanContext
?) -> Unit

Emit an info log.

log_warn

async fn log_warn(scope_name : String, body : String, attributes? : Array[
KeyValue
], trace_context? :
SpanContext
?) -> Unit

Emit a warning log.

logger

Get or create a logger for the given scope name. Logs from this logger go to the default opentelemetry_logs table.

make_random_id_generator

Create an IdGenerator with a process-unique seed.

This helper works around the SDK default RandomIdGenerator's fixed ChaCha8 seed, which produces duplicate trace/span IDs across process restarts.

meter

Get or create a meter for the given scope name.

now_seconds

async fn now_seconds() -> Double

Current wall-clock time in seconds since the Unix epoch.

record_agent_duration

fn record_agent_duration(meter :
Meter
, agent_name : String, seconds~ : Double) -> Unit

Record the duration of one GenAI agent invocation in seconds.

Metric: gen_ai.invoke_agent.duration (Histogram, unit="s")

record_llm_latency

fn record_llm_latency(meter :
Meter
, provider_name : String, model : String, seconds~ : Double) -> Unit

Record the duration of one GenAI chat operation in seconds.

record_tool_call

fn record_tool_call(meter :
Meter
, tool_name : String, success~ : Bool) -> Unit

Record one tool execution.

record_tool_duration

fn record_tool_duration(meter :
Meter
, tool_name : String, seconds~ : Double) -> Unit

Record the duration of one GenAI tool execution in seconds.

Metric: gen_ai.execute_tool.duration (Histogram, unit="s")

record_turn

fn record_turn(meter :
Meter
, max_tool_turns_reached~ : Bool) -> Unit

Record one completed agent turn.

record_usage

fn record_usage(meter :
Meter
, provider_name : String, model : String, token_type : String, value : Int64) -> Unit

Record token usage for one GenAI chat operation.

set_attributes

Set multiple attributes on a span.

set_bool

fn set_bool(span :
Span
, name : String, value : Bool) -> Unit

Set a boolean on a span.

set_double

fn set_double(span :
Span
, name : String, value : Double) -> Unit

Set a floating-point on a span.

set_http_error

fn set_http_error(span :
Span
, status_code : Int, message? : String) -> Unit

Mark a chat span as failed due to an error.

Sets error.type, records the span status as error, and emits a gen_ai.client.operation.exception event following the OTel GenAI semantic conventions.

set_int

fn set_int(span :
Span
, name : String, value : Int64) -> Unit

Set an integer on a span.

set_json

fn set_json(span :
Span
, name : String, value : Json) -> Unit

Set a JSON on a span. The JSON value is serialized to a string, which is useful for structured custom metadata that does not have a dedicated scalar attribute.

set_response

fn set_response(span :
Span
, response_json : Json, output_messages? : Json?, time_to_first_chunk? : Double?) -> Unit

Set response attributes for a successful chat request.

Extracts gen_ai.response.id, gen_ai.response.model and gen_ai.response.finish_reasons from response_json. Also sets gen_ai.output.messages and gen_ai.response.time_to_first_chunk when provided.

set_string

fn set_string(span :
Span
, name : String, value : String) -> Unit

Set a string on a span.

set_tool_error

fn set_tool_error(span :
Span
, description : String) -> Unit

Mark a tool span as failed with a description.

set_tool_result

fn set_tool_result(span :
Span
, result : String) -> Unit

Set the result of a tool execution and mark the span as successful.

set_turn

fn set_turn(span :
Span
, actual_turns : Int, tool_call_count : Int, output : String) -> Unit

Record turn-level attributes before ending an agent turn span.

Sets:
  • agent.turn.actual_turns
  • agent.turn.tool_call_count
  • agent.turn.output

set_turn_exhausted

fn set_turn_exhausted(span :
Span
) -> Unit

Mark an agent turn span as failed due to reaching the maximum tool turns.

set_usage

fn set_usage(span :
Span
, prompt_tokens : Int64, completion_tokens : Int64, cache_read_input_tokens? : Int64, reasoning_output_tokens? : Int64) -> Unit

Set usage attributes from a GenAI response usage object.

Sets gen_ai.usage.input_tokens and gen_ai.usage.output_tokens when present. Also sets gen_ai.usage.cache_read.input_tokens and gen_ai.usage.reasoning.output_tokens when provided.

start_agent_turn_span

Start a span for a single agent turn.

Sets:
  • agent.turn.input
  • agent.turn.max_tool_turns

start_chat_span

fn start_chat_span(tracer :
Tracer
, provider_name : String, model : String, max_tokens : Int, temperature? : Double?, top_p? : Double?, stream? : Bool?, reasoning_level? : String?, stop_sequences? : Array[String]?, frequency_penalty? : Double?, presence_penalty? : Double?, seed? : Int64?, input_messages? : Json?, server_address? : String?, server_port? : Int?, parent_context? :
Context
) ->
Span

Start a span for a GenAI chat request following OpenTelemetry GenAI conventions.

Sets:
  • gen_ai.operation.name = "chat"
  • gen_ai.provider.name
  • gen_ai.request.model
  • gen_ai.request.max_tokens
  • gen_ai.request.temperature (when provided)
  • gen_ai.request.top_p (when provided)
  • gen_ai.request.stream (when provided)
  • gen_ai.request.reasoning.level (when provided)
  • gen_ai.request.stop_sequences (when provided)
  • gen_ai.request.frequency_penalty (when provided)
  • gen_ai.request.presence_penalty (when provided)
  • gen_ai.request.seed (when provided)
  • gen_ai.input.messages (when input_messages is provided)
  • server.address and server.port (when provided)

start_invoke_agent_span

Start a span for a GenAI agent invocation within the same process.

Follows the gen_ai.invoke_agent.internal semantic convention.

Sets:
  • gen_ai.operation.name = "invoke_agent"
  • gen_ai.agent.name (when provided)

Span name: invoke_agent {name} or invoke_agent when name is unavailable.

start_plan_span

Start a span for an agent planning or task decomposition phase.

Follows the gen_ai.plan.internal semantic convention.

Sets:
  • gen_ai.operation.name = "plan"
  • gen_ai.agent.name (when provided)

Span name: plan {name} or plan when name is unavailable.

start_tool_span

fn start_tool_span(tracer :
Tracer
, name : String, arguments : String, call_id? : String?, tool_type? : String?, parent_context? :
Context
) ->
Span

Start a span for a GenAI tool execution following OTel GenAI conventions.

Sets:
  • gen_ai.operation.name = "execute_tool"
  • gen_ai.tool.name
  • gen_ai.tool.call.arguments
  • gen_ai.tool.call.id (when provided)
  • gen_ai.tool.type (when provided)

Span name: execute_tool {name}

tracer

fn tracer(scope_name : String, version? : String) ->
Tracer

Get or create a tracer for the given scope name.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io