This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Observability

How to react to state changes and events in an EDC runtime, and how to monitor it with health checks, logs, traces and metrics

    Overview

    EDC processes are asynchronous. State changes — such as a contract negotiation reaching agreement or a transfer process starting — are communicated through an internal eventing system. There are five ways to observe these state changes from outside the runtime or from within a custom extension:

    ApproachWhere the consumer runsHow it is registered
    In-process event subscriberSame JVM as the EDC runtimeCustom extension, injected EventRouter
    Per-request webhooksExternal HTTP endpointcallbackAddresses in Management API request bodies
    Static callbacksExternal HTTP endpointedc.callback.* configuration properties
    Participant callbacksExternal HTTP endpointedc.callbacks participant context configuration entry
    NATS event publishingNATS subscribersevents-nats extension and edc.events.nats.* configuration properties

    1. In-process event subscribers

    This approach is for custom EDC extensions that need to react to events inside the same runtime process. It requires writing a Java extension that is bundled into your EDC distribution.

    The entry point is the EventRouter service. Register an EventSubscriber against an event type during extension initialization:

    @Inject
    private EventRouter eventRouter;
    
    @Override
    public void initialize(ServiceExtensionContext context) {
        eventRouter.register(TransferProcessEvent.class, new MyTransferEventHandler());
    }
    

    Subscribers are typed over a class hierarchy. Registering against TransferProcessEvent (a base class) will receive all transfer-process-related events. Registering against a concrete class such as TransferProcessStarted will only receive that specific event.

    1.1 Async vs. sync dispatch

    EventRouter offers two dispatch modes:

    • register (async): The subscriber is invoked on a separate thread. The main processing thread is not blocked. Use this for notifications and fire-and-forget scenarios.
    • registerSync (sync): The subscriber is invoked on the same thread that published the event. A failure or exception in the subscriber propagates to the caller and can roll back a transaction. Use this when you need reliable, at-least-once delivery (e.g. persisting events to an audit log).

    1.2 Example: auditing extension

    public class AuditingExtension implements ServiceExtension {
    
        @Inject
        private EventRouter eventRouter;
    
        @Override
        public void initialize(ServiceExtensionContext context) {
            // sync so that audit entries are written within the same transaction
            eventRouter.registerSync(TransferProcessEvent.class, new AuditingEventHandler());
        }
    }
    
    public class AuditingEventHandler implements EventSubscriber {
    
        @Override
        public <E extends Event> void on(EventEnvelope<E> envelope) {
            if (envelope.getPayload() instanceof TransferProcessEvent event) {
                // write event to audit store
            }
        }
    }
    

    For a full explanation of the eventing model, including how to emit custom events, see the Service Layers documentation.

    2. Webhooks (per-request callbacks)

    When you cannot bundle a custom extension into the runtime, you can supply an HTTP callback URL directly in the Management API request that initiates a process. EDC will POST the event payload to that URL when the corresponding state change occurs.

    Callback addresses are provided in the callbackAddresses array of the request body. For example, when initiating a contract negotiation:

    // POST /v4/contractnegotiations
    {
      "@context": ["https://w3id.org/edc/connector/management/v2"],
      "@type": "ContractRequest",
      "counterPartyAddress": "http://provider-address",
      "protocol": "dataspace-protocol-http",
      "policy": { "...": "..." },
      "callbackAddresses": [
        {
          "uri": "https://my-backend/edc-events",
          "events": ["contract.negotiation", "transfer.process"],
          "transactional": false,
          "authKey": "Authorization",
          "authCodeId": "my-secret-vault-alias"
        }
      ]
    }
    

    The events field accepts dot-separated event type prefixes. A value of "contract.negotiation" matches all contract-negotiation events; a value of "transfer.process.started" matches only the started event.

    This approach is tightly scoped: the webhook is only active for the process initiated by that specific request.

    3. Static callbacks

    Static callbacks are configured at runtime startup and apply globally to all matching events, regardless of which Management API request triggered the process. This is useful when you want a single backend endpoint to receive all events of a given type without modifying individual API calls.

    Static callbacks require the callback-static-endpoint extension to be present in your distribution.

    Each callback is defined under the edc.callback.<name> configuration prefix, where <name> is an arbitrary identifier that groups the settings for one endpoint. Multiple callbacks can be configured by using different names.

    # Register a callback named "audit" that receives all transfer-process events
    edc.callback.audit.uri=https://my-backend/edc-events
    edc.callback.audit.events=transfer.process
    
    # Register a second callback named "monitor" for all contract-negotiation events
    edc.callback.monitor.uri=https://monitoring.internal/hooks
    edc.callback.monitor.events=contract.negotiation
    

    3.1 Configuration reference

    PropertyDescriptionRequiredDefault
    edc.callback.<name>.uriThe HTTP endpoint to callyes—
    edc.callback.<name>.eventsComma-separated list of event type prefixes. Leave empty to receive all events.no(all events)
    edc.callback.<name>.transactionalWhether to invoke the callback synchronously within the processing transaction (see below)nofalse
    edc.callback.<name>.auth.keyThe HTTP header name to use for authenticationno—
    edc.callback.<name>.auth.codeidVault alias of the secret token to send in the authentication headerno—

    3.2 Authentication

    If the target endpoint requires authentication, provide the header name in auth.key and store the secret in EDC’s vault. The auth.codeid property is then set to the vault alias under which the secret is stored.

    edc.callback.audit.uri=https://my-backend/edc-events
    edc.callback.audit.events=transfer.process
    edc.callback.audit.auth.key=Authorization
    edc.callback.audit.auth.codeid=my-api-key-alias
    

    auth.codeid is required when auth.key is set. Providing auth.key without auth.codeid will cause the runtime to fail at startup.

    Static callbacks belong to the runtime, so their secrets are always resolved from the default vault partition, even for events of a specific participant context. Per-request webhooks and participant callbacks resolve their secrets from the vault partition of the participant context that owns the event instead.

    3.3 Transactional callbacks

    Setting transactional=true causes the callback to be dispatched synchronously, within the transaction that triggered the event. A failure (e.g. the endpoint is unreachable) will propagate back as a processing error and may cause the state machine to retry the transition. Use this only when the callback endpoint is highly reliable and you need guaranteed delivery. For most monitoring use cases, leave transactional=false.

    4. Participant callbacks

    Participant callbacks are configured per participant context, and apply to all the matching events of that participant context. They are useful when the events are not triggered by a Management API request of the participant, so no per-request webhook can be attached: for example, a provider negotiation created by a contract request of a counterparty, which can be held for manual approval.

    The callbacks are read from the edc.callbacks entry of the participant context configuration. Its value is a JSON array of callback addresses, with the same fields as the per-request webhooks:

    [
      {
        "uri": "https://approvals.example.com/hook",
        "events": [
          "contract.negotiation.held",
          "contract.negotiation.approved",
          "contract.negotiation.rejected"
        ],
        "transactional": false,
        "authKey": "Authorization",
        "authCodeId": "approvals-api-key"
      }
    ]
    
    FieldDescriptionRequiredDefault
    uriThe HTTP endpoint to callyes—
    eventsEvent type prefixes to subscribe toyes—
    transactionalWhether to invoke the callback within the processing transaction (see Transactional callbacks)nofalse
    authKeyThe HTTP header name to use for authenticationno—
    authCodeIdAlias of the secret to send in the authentication header, resolved from the participant context’s vault partitionno—

    In the virtual runtime, the entry is set through the participant context configuration Management API. In the classic runtime, the participant context configuration is the runtime configuration, so edc.callbacks is set as a regular setting.

    Changes to the configuration apply immediately, as the callbacks are not stored on the processes. An invalid edc.callbacks value is logged as a warning and ignored, so it never affects event processing.

    5. Publishing events to NATS

    The events-nats extension publishes every event of the runtime to a NATS JetStream stream, from which any number of applications can consume them. It is not part of the BOMs: add the module org.eclipse.edc:events-nats to the runtime.

    SettingDefaultDescription
    edc.events.nats.url(required)URL of the NATS server
    edc.events.nats.stream(required)name of the stream
    edc.events.nats.stream.createtruecreates the stream at startup; startup fails if it already exists
    edc.events.nats.stream.create.forcefalsedeletes and recreates an existing stream

    The stream created by the runtime uses memory storage and interest retention, for testing; in production, create the stream yourself, on the subjects events.>, and set edc.events.nats.stream.create=false.

    Each event is published asynchronously, on the subject events.<event name>, e.g. events.contract.negotiation.finalized or events.transfer.process.started, as a CloudEvent (Content-Type: application/cloudevents+json) whose type is the event class and data the event payload. The message headers carry the trace context of the event (see Tracing). Publishing errors are logged, not retried: use callbacks when every event must be delivered.

    To authenticate with an NKey, add the module nats-auth-nkey and set edc.nats.auth.nkey.seed.path to the file holding the seed; the same credentials are used for all the NATS connections of the runtime.

    Choosing the right approach

    • Use in-process event subscribers when you are already writing a custom EDC extension and need the lowest latency or transactional semantics.
    • Use per-request webhooks when you want to track specific operations triggered from a controller or orchestration layer that already constructs the Management API calls.
    • Use participant callbacks when each participant context needs its own sink for a class of events, including processes that are not started through the Management API, such as provider contract negotiations.
    • Use static callbacks when you want a centralized, always-on sink for a class of events without modifying individual API requests or shipping code to the runtime.
    • Use NATS event publishing when several applications consume the events of the runtime, or when they need to replay them, and occasional losses are acceptable.

    Health checks

    The api-observability extension (part of the base BOMs) serves health checks on the default API context (web.http.port, default 8181, and web.http.path, default /api):

    EndpointUse
    GET /api/check/startupstartup probe: the runtime has booted
    GET /api/check/livenessliveness probe
    GET /api/check/readinessreadiness probe
    GET /api/check/healthsame as the startup check

    They answer 200 when every check is successful and 503 otherwise, with the result of each check:

    {
      "componentResults": [
        { "component": "BaseRuntime", "isHealthy": true, "failure": null }
      ],
      "isSystemHealthy": true
    }
    

    The checks contributed by EDC are: the boot of the runtime (startup), the poll-based state machines and the NATS task subscribers (liveness: they report a failure when they stopped running), and the HashiCorp Vault connection. The database is not checked. Extensions can contribute their own checks through the HealthCheckService.

    In Kubernetes, use them as probes:

    startupProbe:
      httpGet:
        path: /api/check/startup
        port: 8181
    livenessProbe:
      httpGet:
        path: /api/check/liveness
        port: 8181
    readinessProbe:
      httpGet:
        path: /api/check/readiness
        port: 8181
    

    Version information

    GET /api/v1/version, on the default API context, lists the APIs the runtime serves and their versions:

    {
      "management": [
        { "version": "4.3.0", "urlPath": "/v4", "lastUpdated": "2026-10-29T18:00:47Z", "maturity": "stable" },
        { "version": "5.2.0", "urlPath": "/v5", "lastUpdated": "2026-10-29T18:00:47Z", "maturity": "stable" }
      ]
    }
    

    Logging

    By default, the runtime logs to the standard output, one line per message, with its name, the level and the time:

    [controlplane] INFO 2026-11-04T10:15:30.123 Runtime 2a6c... ready
    

    The level is set with the program argument --log-level (SEVERE, WARNING, INFO, the default, or DEBUG), e.g. java -jar controlplane.jar --log-level=DEBUG; --no-color disables the colors. Errors (SEVERE) are always logged.

    Logs can be sent to an OpenTelemetry collector instead, with the otel-monitor extension (in controlplane-feature-observability-bom) and the OpenTelemetry Java agent: each log message becomes an OpenTelemetry log record of the logger org.eclipse.edc, with the exception, if any, as attributes. Other log formats are provided by extensions implementing MonitorExtension.

    Tracing

    EDC is instrumented for OpenTelemetry tracing, which is enabled by running the runtime with the OpenTelemetry Java agent and configured with the agent’s settings, e.g.:

    java -javaagent:opentelemetry-javaagent.jar \
         -Dotel.service.name=controlplane \
         -Dotel.exporter.otlp.endpoint=http://otel-collector:4318 \
         -jar controlplane.jar
    

    The agent traces the incoming and outgoing HTTP requests (Jetty, Jersey, OkHttp) and the database calls, and EDC adds spans for the incoming DSP messages, the processing of the contract negotiation and transfer process states, and the negotiations started through the Management API. The trace context of a negotiation or transfer is stored with it, so that its later steps belong to the same trace, with both the poll-based and the task-based state machines. Events published to NATS carry the trace context of the step that emitted them.

    Tracing has a cost: in load tests, the agent with full instrumentation reduced the throughput of the control plane noticeably. Sample traces (otel.traces.sampler) in production.

    Metrics

    The Micrometer extensions micrometer-core, jetty-micrometer and jersey-micrometer collect JVM, HTTP client, executor, Jetty and Jersey metrics, see Instrumentation with Micrometer. Add them, together with otel-monitor, with the controlplane-feature-observability-bom, which works with both the single and the multi-participant control planes.

    The Jersey metrics measure the requests of the default API context; list the contexts to measure in edc.metrics.jersey.contexts, e.g. management,protocol,signaling, or * for all of them.

    EDC doesn’t expose the metrics itself: export them with the OpenTelemetry Java agent, e.g. with -Dotel.instrumentation.micrometer.enabled=true and -Dotel.metrics.exporter=prometheus, which serves them for Prometheus on port 9464.