Lifecycle Hooks

The CommandTracingListener interface lets you hook into command lifecycle events without modifying the tracing library itself.

Use cases

  • Setting cmdUuid and cmdSourceRef in MDC for log correlation

  • Starting and stopping OpenTelemetry spans

  • Collecting custom metrics per command

Interface

public interface CommandTracingListener {

    default void onCommandStarted(Command command) {}

    default void onCommandCompleted(Command command) {}

    default void onCommandFailed(Command command, Throwable error) {}

    default void onCommandFailed(Command command, Problem problem) {}
}

All methods have default no-op implementations — implement only what you need.

The two onCommandFailed overloads

The Throwable overload is called in the interceptor’s direct tracing path (non-HTTP and spawned commands), where the original exception is available.

The Problem overload is called in the HTTP filter path, where the exception has already been serialized to a problem+json response. By default, the Problem overload delegates to the Throwable overload via problem.toThrowable(), so listeners that only implement the Throwable variant still receive failure notifications from both paths.

Example: MDC integration

import jakarta.inject.Singleton;
import org.slf4j.MDC;

import no.conta.command.Command;
import no.conta.command.tracing.CommandTracingListener;
import no.conta.problem.Problem;

@Singleton
public class MdcCommandTracingListener implements CommandTracingListener {

    @Override
    public void onCommandStarted(Command command) {
        MDC.put("cmdUuid", String.valueOf(command.getCmdUuid()));
        MDC.put("cmdSourceRef", command.getCmdSourceRef());
    }

    @Override
    public void onCommandCompleted(Command command) {
        MDC.remove("cmdUuid");
        MDC.remove("cmdSourceRef");
    }

    @Override
    public void onCommandFailed(Command command, Throwable error) {
        MDC.remove("cmdUuid");
        MDC.remove("cmdSourceRef");
    }
}

Register as a Micronaut bean (@Singleton) and the interceptor and filter will discover it automatically.

How it works

The listener callbacks fire at different points depending on the execution context:

Non-HTTP path (direct tracing)

The interceptor calls all three callbacks directly: onCommandStarted before execution, onCommandCompleted after success, and onCommandFailed(Command, Throwable) on exception.

HTTP-delegated path

onCommandStarted fires in the interceptor before proceed(). onCommandCompleted or onCommandFailed(Command, Problem) fires in CommandTracingFilter when the response is returned. Both depend on CommandTracingContext propagation — the same constraint that applies to event publishing.

Important constraints

  • Callbacks execute on the calling thread (the Netty event loop in the HTTP path). Implementations must not block.

  • If a listener throws an exception, it is logged and the remaining listeners still receive the notification.