Observability
Geblang provides lightweight observability modules for scripts, services, tests, and framework code:
logfor structured logging to stdout, stderr, files, or custom handlers.metricsfor in-process counters, gauges, and simple timings.tracefor span-style request/workflow traces.profilefor runtime memory and GC diagnostics.
These modules are intentionally small. They provide useful defaults and clear extension points without requiring a collector, daemon, or external service.
Logging
Import log for structured log entries. A logger is a runtime handle returned
by log.stdout, log.stderr, log.file, or log.custom.
import log;
let logger = log.stdout();
log.info(logger, "server started", {"port": 8080});
Built-In Logger Destinations
| Function | Returns | Description |
|---|---|---|
stdout() |
logger handle | Write JSON log lines to stdout |
stderr() |
logger handle | Write JSON log lines to stderr |
file(path) |
logger handle | Append JSON log lines to a file |
close(logger) |
void |
Close and unregister a logger |
let out = log.stdout();
let err = log.stderr();
let file = log.file("/var/log/app.log");
defer log.close(file);
log.info(out, "ready");
log.warn(err, "using fallback config", {"path": "config/local.yaml"});
log.error(file, "request failed", {"status": 500, "path": "/api/users"});
Built-in loggers emit one JSON object per line. The exact field order is not part of the API, but entries include at least:
| Field | Type | Description |
|---|---|---|
level |
string |
info, warn, error, or debug |
message |
string |
Log message |
fields |
dict |
Structured fields passed by the caller |
timeUnix |
int |
Timestamp as Unix seconds |
Levels
| Function | Description |
|---|---|
info(logger, message) |
Informational event |
info(logger, message, fields) |
Informational event with fields |
warn(logger, message) |
Warning event |
warn(logger, message, fields) |
Warning event with fields |
error(logger, message) |
Error event |
error(logger, message, fields) |
Error event with fields |
debug(logger, message) |
Debug event |
debug(logger, message, fields) |
Debug event with fields |
fields must be a dictionary. Keep fields machine-readable: prefer ids,
status codes, durations, paths, and booleans over preformatted message text.
log.info(logger, "user login", {
"userId": "42",
"ip": request["remoteAddr"],
"remember": true
});
log.LogInterface
Custom handlers can implement the exported log.LogInterface. Its required
method has this shape:
func handle(string level, string message, dict<string, any> fields): void;
Use it when a class should be accepted by logging-aware code or when you want compile-time checking of the handler shape.
import io;
import json;
import log;
class JsonSink implements log.LogInterface {
func handle(string level, string message, dict<string, any> fields): void {
io.println(json.stringify({
"level": level,
"message": message,
"fields": fields
}));
}
}
let logger = log.custom(JsonSink());
log.info(logger, "custom logger ready", {"handler": "json"});
log.custom(handler) still checks structurally for a compatible handle
method, but implementing log.LogInterface makes the contract explicit:
class MemoryLogger implements log.LogInterface {
list<dict<string, any>> entries = [];
func handle(string level, string message, dict<string, any> fields): void {
this.entries = this.entries.push({
"level": level,
"message": message,
"fields": fields
});
}
}
let sink = MemoryLogger();
let logger = log.custom(sink);
log.error(logger, "validation failed", {"field": "email"});
io.println(sink.entries[0]["message"]);
Custom handlers are useful for:
- writing to external services;
- forwarding logs to test assertions;
- adapting logs into framework-specific event systems;
- redacting or transforming fields before output.
Logger Lifecycle
Call log.close(logger) when a logger is no longer needed. File loggers close
their underlying file handle. Stdout/stderr/custom loggers unregister the
runtime handle.
let file = log.file("app.log");
defer log.close(file);
Do not write to a logger after closing it; the runtime will report an unknown logger handle.
Logging Patterns
Create a request logger:
func logRequest(any logger, dict<string, any> req, int status, int durationMs): void {
log.info(logger, "http request", {
"method": req["method"],
"path": req["path"],
"status": status,
"durationMs": durationMs
});
}
Capture logs in tests:
class Capture implements log.LogInterface {
list<dict<string, any>> entries = [];
func handle(string level, string message, dict<string, any> fields): void {
this.entries = this.entries.push({
"level": level,
"message": message,
"fields": fields
});
}
}
let capture = Capture();
let logger = log.custom(capture);
log.warn(logger, "rate limit", {"limit": 100});
io.println(capture.entries.length());
Metrics
Import metrics for process-local counters and gauges. Metric names are
free-form strings; use dot-separated names by convention:
"http.requests", "jobs.completed", "db.pool.open".
Recording Values
| Function | Returns | Description |
|---|---|---|
inc(name) |
void |
Increment a counter by 1 |
inc(name, amount) |
void |
Increment by an integer amount |
set(name, value) |
void |
Set a metric to an absolute numeric value |
reset() |
void |
Clear all metrics |
metrics.inc("jobs.completed");
metrics.inc("bytes.sent", 4096);
metrics.set("queue.depth", 12);
Reading Values
| Function | Returns | Description |
|---|---|---|
get(name) |
number | Current value for one metric |
snapshot() |
dict<string, number> |
Copy of all metrics |
let jobs = metrics.get("jobs.completed");
let all = metrics.snapshot();
Timing
| Function | Returns | Description |
|---|---|---|
now() |
opaque timestamp | Monotonic timestamp |
duration(start) |
int |
Milliseconds since start |
let start = metrics.now();
# work
metrics.set("job.durationMs", metrics.duration(start));
Metrics are in-process. Export snapshots to logs, HTTP endpoints, or external collectors when you need persistence.
Trace
Import trace for lightweight span-based tracing. Spans are handles while they
are active and dictionaries after snapshot.
Span API
| Function | Returns | Description |
|---|---|---|
start(name) |
span handle | Start a span |
event(span, name, fields) |
void |
Attach an event |
end(span) |
void |
Finish a span |
snapshot() |
list<dict> |
Completed spans |
reset() |
void |
Clear completed spans |
let span = trace.start("load-users");
trace.event(span, "query", {"table": "users"});
trace.event(span, "marshal", {"format": "json"});
trace.end(span);
let spans = trace.snapshot();
Completed span dictionaries contain:
| Key | Type | Description |
|---|---|---|
name |
string |
Span name |
startUnix |
int |
Start timestamp |
endUnix |
int |
End timestamp |
durationMs |
int |
Duration in milliseconds |
events |
list<dict> |
Span events |
Event dictionaries contain name, fields, and timeUnix.
Profile
Import profile for runtime diagnostics. These helpers are useful for
debugging memory-heavy scripts and checking allocation pressure.
Memory Stats
| Function | Returns | Description |
|---|---|---|
memStats() |
dict |
Runtime memory and GC counters |
gc() |
void |
Force a garbage collection cycle |
let mem = profile.memStats();
io.println("heap alloc bytes: " + mem["heapAlloc"] as string);
io.println("sys bytes: " + mem["sys"] as string);
io.println("gc cycles: " + mem["numGC"] as string);
Common memStats fields include heapAlloc, heapSys, heapInuse,
heapObjects, sys, numGC, and pauseTotalNs.
Timing
| Function | Returns | Description |
|---|---|---|
now() |
opaque timestamp | Monotonic timestamp |
elapsed(start) |
float |
Milliseconds since start |
let start = profile.now();
# work
let ms = profile.elapsed(start);
io.println("elapsed ms: " + ms as string);
Profiler
Import profiler for precise CPU time and heap measurements from a native
Go runtime interface. Use profiler.snapshot() and profiler.delta() together
to bracket a section of code, or call profiler.memory() and profiler.cpu()
for one-off readings.
This module is always available as a native module and does not require
importing from stdlib/.
Functions
| Function | Returns | Description |
|---|---|---|
snapshot() |
dict |
Captures wall clock (wall_ns), heap allocation (heap_alloc, peak_alloc, total_alloc), GC count (num_gc), and CPU nanoseconds (cpu_user_ns, cpu_sys_ns) |
delta(snapshot) |
dict |
Returns measurements since the snapshot: elapsed_ms, cpu_ms, heap_alloc, allocs, gc_count |
memory() |
dict |
Returns heap_alloc, peak_alloc, heap_sys, stack_sys, total_alloc, gc_count |
cpu() |
dict |
Returns user_ms and sys_ms CPU time used by this process |
peak() |
dict |
Returns peak_alloc: the highest heap allocation observed since the profiler was first called |
peak_alloc tracks the maximum live heap bytes seen across all profiler calls in the
process lifetime - equivalent to PHP's memory_get_peak_usage(). It is updated on every
call to snapshot(), memory(), and peak().
Example
import profiler;
import io;
let snap = profiler.snapshot();
# do some work
let sum = 0;
for i in range(0, 1000000) {
sum = sum + i;
}
let d = profiler.delta(snap);
io.println("elapsed: " + d["elapsed_ms"] as string + " ms");
io.println("cpu: " + d["cpu_ms"] as string + " ms");
io.println("heap delta: " + d["heap_alloc"] as string + " bytes");
io.println("allocations: " + d["allocs"] as string + " bytes total");
io.println("gc cycles: " + d["gc_count"] as string);
For a one-off memory check including peak:
import profiler;
import io;
let mem = profiler.memory();
io.println("heap in use: " + mem["heap_alloc"] as string + " bytes");
io.println("peak heap: " + mem["peak_alloc"] as string + " bytes");
io.println("heap from OS: " + mem["heap_sys"] as string + " bytes");
To check peak memory at the end of a script (similar to memory_get_peak_usage() in PHP):
import profiler;
import io;
# ... script work ...
let p = profiler.peak();
io.println("peak memory: " + p["peak_alloc"] as string + " bytes");