Configuration, Logging & Actuators¶
Write functions: run them the way engine apps run.
At a glance
- What — one configuration style, one log presentation, one operational surface across Java, Rust, Python and Node.js apps.
- For developers wiring an app and operators monitoring a polyglot estate.
Configuration — the engines' conventions¶
Configuration lives in the resources folder (resources/application.yml, .yaml
or .properties), in the working directory or next to the application file. Values
support ${ENV_VAR:default} substitution; -Dkey=value command-line arguments are
runtime overrides checked first on every read — the same syntax as the Java engine's
JVM system properties and the Rust port's -D arguments:
See the worked sample
examples/resources/application.yml
and the full key table in the Configuration Reference.
Logging — one aggregation, three presentations¶
Log lines follow the Java reference engine's pattern, so a polyglot installation reads one way in the aggregator:
log.format carries the engines' three presentations: text (default), json
(pretty-printed) and compact (single-line JSONL for log aggregators). The level
comes from the LOG_LEVEL environment variable when set, else log.level.
Distributed tracing — the OpenTelemetry forwarder (opt-in)¶
Every traced, non-RPC execution emits the engines' distributed-trace dataset on the
distributed.tracing log stream — {"trace": {...}, "annotations": {...}}, the record the
Java engine logs — so a log aggregation already stitches the span tree across all four
runtimes. The forwarder is the second sink, for backends that take spans directly:
Dynatrace, Splunk, an OpenTelemetry Collector.
otel.forwarding: true
otel.exporter.otlp.endpoint: 'https://<tenant>.live.dynatrace.com/api/v2/otlp/v1/traces'
otel.exporter.otlp.headers: 'Authorization=Api-Token ${DT_TOKEN}'
otel.service.name: 'my-node-functions'
With otel.forwarding=true (the switch is the only thing that turns it on — false by
default, and -Dotel.forwarding=true at run time is enough), the host also hands each
dataset to the engines' extension route distributed.trace.forwarder, where the built-in
forwarder registers itself at start-up (private, two workers; an application that registers
its own function on that route wins). Each dataset becomes one OpenTelemetry span carrying
the host's exact W3C trace, span and parent-span ids — the same mapping as the engines'
forwarders, so one trace spans the engine that called and the function here — and is
exported over OTLP/HTTP as protobuf, one span per request, through the runtime's own fetch.
There is no OpenTelemetry SDK and no new dependency: the encoder is the engines' own
hand-written OTLP writer, ported.
Request headers are re-read on every export, so a credential a start-up bootstrap publishes
with config.set(...) after the forwarder started takes effect without a restart; header
names appear in the log, never values. A transport failure or a 408/429/502/503/504
answer is retried five times on the OpenTelemetry SDK's backoff (1 s growing by 1.5×);
any other rejection is logged once with the status, the backend's message and a hint
(HTTP 401 ... | the backend rejected the credential itself - check otel.exporter.otlp.headers).
The start-up line confirms the configuration:
OpenTelemetry trace forwarder ready - service=..., OTLP endpoint=..., compression=none, credential headers=[...].
Differences from the engines: only none compression is honoured (a warning otherwise);
otel.exporter.otlp.connect.timeout has no effect — otel.exporter.otlp.timeout bounds the
whole export, connect included (a warning names it); the instrumentation scope is
mercury-composable-nodejs, so a backend shows which runtime produced a span. The keys are
listed in the configuration reference.
Actuators — the engines' operational surface¶
The host serves the engines' endpoints on the same port as /api/event:
| Endpoint | Purpose |
|---|---|
GET / |
minimal index page linking the endpoints below |
GET /info |
app identity, runtime, origin id, start time, uptime |
GET /info/routes |
registered routes split by visibility, with instance counts |
GET /env |
selected environment variables and configuration parameters (opt-in lists) |
GET /health |
dependency health checks — UP (HTTP 200) or DOWN (HTTP 400) |
GET /livenessprobe |
OK while the last health outcome was good, else HTTP 400 |
JSON responses are pretty-printed (the engines' default-serializer presentation) with
application/json; charset=utf-8; unknown paths answer the engines' error shape —
see the HTTP Surface Reference.
Health check functions — the engines' interface contract¶
A health check is a normal registered function (usually private) listed in
mandatory.health.dependencies / optional.health.dependencies. The actuator calls
it through the event bus, first with header type=info (an advisory identity map
merged into its dependency entry), then with type=health (a status text or map; a
non-200 reply marks the dependency down):
preload('demo.health', { instances: 5, isPrivate: true }, async (headers, _body) => {
if (headers.type === 'info') {
return { service: 'demo.service', href: 'http://127.0.0.1' };
}
return 'demo.service is running fine';
});
Optional dependencies never change the overall status; mandatory ones decide
UP/DOWN, and the most recent outcome drives /livenessprobe.
Kubernetes wiring¶
livenessProbe:
httpGet: { path: /livenessprobe, port: 8087 }
readinessProbe:
httpGet: { path: /health, port: 8087 }
The pod presents exactly like an engine pod — one dashboard shape for the whole polyglot estate.