diff --git a/crates/ffi/README.md b/crates/ffi/README.md index 8874329db..65bf893cb 100644 --- a/crates/ffi/README.md +++ b/crates/ffi/README.md @@ -44,6 +44,8 @@ binding consumes it through CGo. - **Exported `nemo_relay_*` symbols**: APIs for scopes, tool calls, LLM calls, middleware, subscribers, plugins, observability exporters, and scope stack isolation. +- **Event metadata injection**: Register global, scope-local, or plugin-owned + callbacks that inspect an immutable event and propose flat metadata additions. - **Typed OpenTelemetry export**: `nemo_relay_otel_subscriber_create` constructs one `full`, `gen_ai`, or `openinference` trace subscriber. Independently managed log and metric @@ -68,6 +70,55 @@ long-running callback work therefore occupy that thread and can reduce middleware throughput. The FFI does not expose completion-based middleware registration. +## Event Metadata Injection + +An event metadata injector receives a borrowed `FfiEvent` and returns a +heap-allocated JSON object containing proposed metadata additions. Relay frees +the returned string, validates the object, and inserts only keys that are not +already present. Return null after calling +`nemo_relay_set_last_error_message` to reject that callback's additions while +allowing the event to continue through sanitization and delivery. + +The following C example registers application-wide metadata injection: + +```c +#include +#include +#include + +static char *inject_metadata(void *user_data, const FfiEvent *event) { + (void)user_data; + (void)event; + const char payload[] = "{\"application.region\":\"us-central\"}"; + char *result = malloc(sizeof(payload)); + if (result == NULL) { + nemo_relay_set_last_error_message("failed to allocate injector result"); + return NULL; + } + memcpy(result, payload, sizeof(payload)); + return result; +} + +NemoRelayStatus status = nemo_relay_register_event_metadata_injector( + "application-metadata", 10, inject_metadata, NULL, NULL +); +if (status != NEMO_RELAY_STATUS_OK) { + fprintf(stderr, "registration failed: %s\n", nemo_relay_last_error()); + return EXIT_FAILURE; +} +/* Emit scopes and marks while the callback is registered. */ +status = nemo_relay_deregister_event_metadata_injector("application-metadata"); +if (status != NEMO_RELAY_STATUS_OK) { + fprintf(stderr, "cleanup failed: %s\n", nemo_relay_last_error()); + return EXIT_FAILURE; +} +``` + +Use `nemo_relay_scope_register_event_metadata_injector` for an active scope or +`nemo_relay_plugin_context_register_event_metadata_injector` during plugin +registration. The corresponding deregistration functions remove future +invocations; scope and plugin cleanup also remove their owned registrations. + ## OTLP Logs and Metrics The raw C ABI remains experimental and source-first. Configure plugin-managed diff --git a/docs/build-plugins/language-binding/register-behavior.mdx b/docs/build-plugins/language-binding/register-behavior.mdx index 01d708a2f..299233437 100644 --- a/docs/build-plugins/language-binding/register-behavior.mdx +++ b/docs/build-plugins/language-binding/register-behavior.mdx @@ -12,6 +12,72 @@ example installs only the feature groups whose `enabled` values are true and rea configuration. It never calls a process-global middleware registrar from inside the plugin. +## Register Event Metadata Injectors + +An event metadata injector receives an event snapshot and returns flat metadata +additions. Relay validates and inserts accepted additions before event sanitizers run. +Existing metadata values are preserved. + +The following examples register a component-owned callback through `PluginContext`: + + + +```python +def register(self, plugin_config, context): + tag = plugin_config["tag"] + context.register_event_metadata_injector( + "component-metadata", + 10, + lambda event: {"example.plugin.tag": tag}, + ) +``` + +Applications can instead use `nemo_relay.event_metadata.register_injector()` for a +global callback or `nemo_relay.scope_local.register_event_metadata_injector()` for a +callback owned by an active scope. The matching deregistration functions remove those +registrations. + + +```js +register(pluginConfig, context) { + context.registerEventMetadataInjector('component-metadata', 10, () => ({ + 'example.plugin.tag': pluginConfig.tag, + })); +} +``` + +Applications can instead use `registerEventMetadataInjector()` for a global callback +or `scopeRegisterEventMetadataInjector()` for a callback owned by an active scope. The +matching deregistration functions remove those registrations. + + +```rust +let tag = config.tag.clone(); +ctx.register_event_metadata_injector( + "component-metadata", + 10, + Arc::new(move |_event| { + let tag = tag.clone(); + Box::pin(async move { + Ok(BTreeMap::from([( + "example.plugin.tag".into(), + Json::String(tag), + )])) + }) + }), +)?; +``` + +Applications can instead use `register_event_metadata_injector()` for a global +callback or `scope_register_event_metadata_injector()` for a callback owned by +an active scope. The matching deregistration functions remove those registrations. + + + +Python and Node.js callbacks can return additions directly or asynchronously. Rust +callbacks return a future. In every binding, callback failures and invalid return values +omit that callback's additions without dropping the event. + ## Register One Equivalent Request Intercept The following excerpts show the same model-header rewrite. The full checked examples add diff --git a/go/nemo_relay/README.md b/go/nemo_relay/README.md index 7fec6670b..fae1c0f16 100644 --- a/go/nemo_relay/README.md +++ b/go/nemo_relay/README.md @@ -51,6 +51,8 @@ The Go package provides the following capabilities: - **Middleware APIs**: Guardrails and intercepts for request rewriting, blocking, sanitization, and execution wrapping, including mark and scope event sanitizers at global, scope-local, and plugin-context levels. +- **Event metadata injection**: Global, scope-local, and plugin-context + callbacks can inspect immutable events and propose flat metadata additions. - **Event subscribers**: Runtime lifecycle callbacks for observability and diagnostics. - **Typed OpenTelemetry export**: `NewOpenTelemetryConfig` returns configuration @@ -74,6 +76,36 @@ native thread, so blocking I/O and other long-running callback work occupy that thread and can reduce middleware throughput. The Go binding does not provide completion-based middleware registration. +## Event Metadata Injection + +Use `RegisterEventMetadataInjector` for application-wide metadata. The callback +receives an owned event snapshot and returns proposed additions. Relay validates +the map, preserves existing metadata, and applies injectors in priority order. +Returning an error rejects only that callback's additions; the event is still +delivered. + +The following Go example registers application-wide metadata injection: + +```go +err := nemo.RegisterEventMetadataInjector( + "application-metadata", + 10, + func(event nemo.Event) (nemo.EventMetadata, error) { + return nemo.EventMetadata{ + "application.event_kind": event.Kind(), + }, nil + }, +) +if err != nil { + log.Fatal(err) +} +defer nemo.DeregisterEventMetadataInjector("application-metadata") +``` + +Use `ScopeRegisterEventMetadataInjector` for an active scope. Plugins use +`PluginContext.RegisterEventMetadataInjector` so component name qualification +and rollback cleanup apply to the registration. + ## OTLP Logs and Metrics Use `EmitEvent` with `WithEventDataSchema` and `WithEventSeverity` for a typed