This proposal introduces a capability-based extension system for the OTel Dataflow Engine.
Extensions allow receivers, processors, and exporters to access non-pdata functionality, such as:
- authentication
- identity extraction
- token sources
- external data sources (file, database, etc.)
- enrichment services
The system is introduced incrementally in three phases.
The extension system should:
-
allow nodes to access capabilities exposed by extensions
-
allow multiple implementations of the same capability
-
allow multiple configured instances of providers in the same pipeline
-
integrate cleanly into the existing configuration model
-
provide a clear user experience for configuring extensions
-
provide a clear developer experience for implementing extensions
-
preserve the engine's performance model:
- thread-per-core execution
- minimal synchronization
- local hot-path access
-
support future hierarchical scopes (global, group, pipeline)
-
allow extensions to run background tasks
The system is not intended to:
- replace pdata processing nodes
- introduce dynamic runtime loading in phase 1
- allow external extensions to define new capability interfaces/traits
Capability interfaces (traits) are defined and maintained in the OTel Dataflow Engine core. Extensions implement these predefined capabilities but cannot introduce new capability interfaces outside the core.
A capability is a typed interface/trait that nodes can use.
Examples:
auth_check
dataset_lookup
Capabilities are known by the engine version and validated during configuration loading.
This ensures:
- deterministic configuration validation
- compatibility guarantees
- clear error reporting
An extension provider is an extension implementation exposing one or more capabilities.
Example extension providers:
| Extension Provider | Capabilities |
|---|---|
basic_auth provider |
auth_check |
bearer_token_auth provider |
auth_check |
oidc_auth provider |
auth_check |
file_storage provider |
dataset_lookup |
db_storage provider |
dataset_lookup |
Different extension providers may implement the same capability with different logic or external dependencies.
An extension instance is a configured extension provider declared in the
extensions section.
Example:
extensions:
auth_main:
type: extension:oidc_auth
config:
issuer: https://accounts.example.comMultiple instances may exist using different configurations or implementations.
The execution model of an extension (for example local per core or shared across cores) is defined by the extension provider implementation, not by the configuration. The placement of the extension declaration in the configuration hierarchy remains an orthogonal concern handled by the runtime.
Nodes bind capabilities to extension instances.
Example:
nodes:
otlp_recv1:
type: receiver:otlp
capabilities:
auth_check: auth_main
config:
protocols:
grpc:
listening_addr: "127.0.0.1:4327"This creates a mapping:
node -> capability -> extension instance
This approach improves:
- configuration clarity
- validation
- flexibility
The extension system integrates directly into the engine's configuration hierarchy.
For phase 1, extensions are declared at the pipeline level and consumed by nodes within that pipeline.
Example:
version: otel_dataflow/v1
groups:
continuous_benchmark:
pipelines:
sut:
extensions:
oidc_auth_main:
type: extension:oidc_auth
config:
issuer: https://accounts.example.com
local_auth:
type: extension:basic_auth
config:
file: /etc/auth/tokens.yaml
nodes:
otlp_recv1:
type: receiver:otlp
capabilities:
auth_check: oidc_auth_main
config:
protocols:
grpc:
listening_addr: "127.0.0.1:4327"
otlp_recv2:
type: receiver:otlp
capabilities:
auth_check: local_auth
config:
protocols:
grpc:
listening_addr: "127.0.0.1:4337"This model keeps extension usage explicit and consistent with the existing
groups -> pipelines -> nodes structure.
From the user perspective, the extension system should be:
- explicit
- predictable
- discoverable
Users should be able to:
- Declare extension instances in the configuration
- Bind node capabilities to those instances
Developers implementing extensions should have a simple integration model.
An extension provider implementation should define:
- provider name
- supported capabilities
- supported execution model (local or shared)
- configuration schema
- runtime logic
Extensions may also run background tasks.
Example:
oidc_auth provider
|- capability: auth_check
`- background task: JWKS refresh
Extension developers should focus on provider logic, not runtime plumbing.
A key objective is to make extensions self-descriptive and discoverable.
Extension metadata should expose:
| Field | Description |
|---|---|
| name | extension urn identifier |
| description | human-readable description |
| capabilities | supported capabilities |
| supported_execution_model | shared/local |
| documentation_url | usage documentation |
This enables:
- extension catalog generation
- automatic documentation
- CLI inspection
Extensions register themselves using the same mechanism used for node discovery.
The engine already relies on the distributed_slice crate for node
implementation discovery.
The same approach can be adopted for extensions.
Conceptually:
Extension implementation
|
v
distributed_slice registration
|
v
extension registry
|
v
capability catalog
The runtime system consists of:
- extension declarations (configuration)
- runtime units (instantiated extension providers)
- capability handles used by nodes
Config
|
v
Extension declarations
|
v
Extension Runtime Manager
|
v
Extension runtime units
|
v
Capability handles
|
v
Nodes
Nodes resolve capability handles during initialization only.
There is no registry lookup on the hot path.
The engine runs one pipeline instance per core.
Core 0 Core 1 Core 2 Core 3
| | | |
Pipeline Pipeline Pipeline Pipeline <- All configured from the same config
Instance Instance Instance Instance
The execution model of an extension (local per core or shared across cores) is determined by the extension provider implementation, not by user configuration.
In phase 1, extensions are declared at the pipeline level and consumed by nodes within that pipeline.
Two execution models are supported by extension providers.
| Execution Model | Description |
|---|---|
local |
One runtime instance per core |
shared |
One runtime instance shared across cores |
The supported model is declared by the extension provider implementation.
Local execution allows extension implementations to remain thread-local.
In many cases this allows implementations without:
Arc
Mutex
and instead rely on:
Rc
RefCell
Cell
This avoids cross-core synchronization and preserves the engine's performance characteristics.
Features:
- pipeline-level extensions
- capability binding in nodes
- background tasks supported
Adds:
-
extension declarations at
- top/engine-level
- group-level
Possible future distributed scope.
Adds:
- WASM runtime for providers
- sandboxed execution
- host capability ABI
This proposal introduces a capability-based extension architecture aligned with the OTel Dataflow Engine's high-performance design and configuration model.
The phased rollout allows the system to evolve gradually while keeping the initial implementation small and focused.
The result is a flexible extension mechanism that preserves performance, clarity, and long-term extensibility.
Characteristics:
- extensions are global components
- accessed indirectly by receivers/processors/exporters
Pros:
- simple architecture
- mature ecosystem
Cons:
- limited scoping flexibility
- not optimized for thread-per-core execution
Characteristics:
- capability-based
- explicit capability binding in nodes
- thread-per-core and NUMA-aware architecture
Pros:
- explicit capability contracts
- optimized for thread-per-core runtime
- flexible extension scoping
- clearer configuration semantics
Cons:
- slightly more configuration
- ecosystem still developing
This appendix provides implementation guidance intended to help extension authors build something performant, aligned with the supported scopes, and compatible with the engine architecture.
Capability handles are expected to be used by nodes on hot paths. They should therefore be:
- cheap to clone
- cheap to pass around during initialization
- inexpensive to call
- free of runtime registry lookups
In practice, the handle should behave like a thin reference to already-resolved extension state.
They can also include local cached state for hot-path usage, with background tasks responsible for refreshing that state as needed.
Capability binding should happen during node initialization only.
Nodes should receive typed capability handles once and keep them for their lifetime. They should not perform dynamic capability lookups during runtime.
If a capability is frequently used during request or batch processing, local
execution model should generally be preferred.
This gives:
- better cache locality
- more predictable latency
- no cross-core synchronization on the hot path
For local execution model, implementations should be designed so they can
often rely on thread-local ownership and avoid Arc<Mutex<...>>.
Typical building blocks for local implementations include:
RcRefCellCell
This is one of the main performance advantages of local extensions in a thread-per-core engine.
Extension providers may need to:
- refresh external state
- reload files
- fetch JWKS data
- query a remote service
- update caches
These activities should happen in background tasks rather than directly on the node hot path.
A typical pattern is:
- node uses a lightweight capability handle
- handle reads local cached state
- background task refreshes or reloads that state asynchronously
For non-local pipeline scope and future broader scopes, implementations should
avoid designs where every capability call requires cross-core communication.
A better pattern is usually:
- shared ownership for coordination or refresh
- local read views or local cached snapshots for hot-path usage
Capabilities should expose only what nodes need.
Small capability interfaces are easier to:
- validate
- document
- optimize
- evolve over time
Because extension metadata may be used to generate documentation and catalogs, it should remain up to date and include at least:
- description
- supported capabilities
- supported scopes
- documentation URL
Note: We probably want to enforce this with a macro to register extensions, which would require metadata fields to be provided at compile time.