Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions crates/ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
Comment thread
ericevans-nv marked this conversation as resolved.
}

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;
}
```
Comment thread
ericevans-nv marked this conversation as resolved.

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
Expand Down
66 changes: 66 additions & 0 deletions docs/build-plugins/language-binding/register-behavior.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

<Tabs>
<Tab title="Python" language="python">
Comment thread
ericevans-nv marked this conversation as resolved.
```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.
</Tab>
<Tab title="Node.js" language="node">
```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.
</Tab>
<Tab title="Rust" language="rust">
```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.
</Tab>
</Tabs>

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
Expand Down
32 changes: 32 additions & 0 deletions go/nemo_relay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading