Package bootstrap

Overview

MVX Logger is usable immediately after import.

The package creates a default logging setup during module bootstrap:

import mvx.common.logger
        |
        v
create sink registry
        |
        v
register default stderr sink
        |
        v
create root LogContext
        |
        v
create context registry

This gives the package a ready root context and a ready default sink without requiring application code to configure anything first.

At the same time, the default setup is not a closed box. Applications can register additional sinks, configure namespace contexts, reset non-root contexts, or reset the whole package-level logger state.

Why it exists

The logger needs to work in two different modes.

First, it should be immediately useful:

from mvx.common.logger import get_root_log_context

ctx = get_root_log_context()
ctx.log_info_event(event="app.started", payload={})

Second, it should remain configurable for larger applications:

configure_log_sink(name="file", sink_cls=FileLogSink, ...)
configure_log_context("my_app.db", log_sink=file_sink)

Bootstrap provides the first mode. Registries and public configuration functions provide the second.

The design avoids forcing every consumer to write setup code before the first event, while still keeping explicit configuration available.

Bootstrap sequence

Package bootstrap is implemented by _bootstrap().

It creates two registries:

_LogSinkRegistry
_LogContextRegistry

The sequence is:

1. Create an empty sink registry.
2. Register the default root sink under the name "stderr".
3. Create the root LogContext.
4. Create the context registry with that root context.
5. Store both registries as package-level state.

In code shape:

log_sink_registry = _LogSinkRegistry()

log_sink = log_sink_registry.register(
    name=DEFAULT_ROOT_LOG_SINK_NAME,
    sink_cls=StreamLogSink,
)

root_ctx = LogContext(
    namespace=ROOT_LOG_CONTEXT_NAMESPACE,
    log_sink=log_sink,
    payload_processor=LogPayloadProcessor(),
)

log_context_registry = _LogContextRegistry(root_ctx)

If bootstrap fails, the package writes a last-resort internal error to stderr and re-raises the exception.

Default constants

The default root namespace is the empty string:

ROOT_LOG_CONTEXT_NAMESPACE = ""

The default sink name is:

DEFAULT_STDERR_LOG_SINK_NAME = "stderr"
DEFAULT_ROOT_LOG_SINK_NAME = DEFAULT_STDERR_LOG_SINK_NAME

So the default root context is backed by a package-registered sink named "stderr".

The empty root namespace is internal to the context tree. Public namespace validation for user-created contexts requires a non-empty dotted namespace.

Default root context

The default root context is created with:

namespace          -> ""
log_sink           -> default StreamLogSink registered as "stderr"
payload_processor  -> LogPayloadProcessor()
parent             -> None

The root context is the mandatory fallback for the package-level context tree.

It must always have a sink and a payload processor.

Child contexts may inherit these components from the root unless they define local overrides.

Default sink

The default sink is created through the same registry mechanism as any other package-managed sink.

Bootstrap calls:

log_sink_registry.register(
    name="stderr",
    sink_cls=StreamLogSink,
)

This means the default sink is not a special case after creation. It is stored in the sink registry together with its descriptor and terminator.

It can be retrieved by name:

sink = get_log_sink("stderr")

It is also protected by the same in-use rules as other registered sinks.

Because the root context locally owns the default sink, close_log_sink("stderr") cannot close it while it is assigned to the root context.

Sink registry

The sink registry stores package-managed sinks by name.

Each registered sink entry contains:

sink
terminator
descriptor

The registry uses two locks internally:

lifecycle lock
registry lock

The lifecycle lock protects create/reset/unregister flows.

The registry lock protects access to the dictionary of registered sinks.

The registry supports:

register
get
get_sinks_names
is_empty
unregister
reset

Public functions expose only the package-level API, not the registry object itself.

Sink registration identity

configure_log_sink() delegates to the sink registry.

The registry first asks the sink class to build a descriptor:

descriptor = sink_cls.build_descriptor(**sink_kwargs)

Then it checks whether the name is already registered.

The behavior is:

name not registered
    -> create and store a new sink

name registered with the same descriptor
    -> return the existing sink

name registered with a different descriptor
    -> raise LogSinkConfigurationConflictError

This makes sink configuration idempotent when the request is identical and explicit when a name conflict exists.

The registry does not silently replace existing sinks.

Context registry

The context registry stores package-managed LogContext instances by namespace.

It is initialized with the root context:

self._contexts = {ROOT_LOG_CONTEXT_NAMESPACE: root_log_context}

The registry supports:

get_root_log_context
get
put
contains
list_namespaces
clear
get_contexts_by_log_sink
create_log_context_chain

Like the sink registry, it is internal. Public functions expose package-level access.

Context namespace chain creation

When configure_log_context() receives a namespace that does not exist, the context registry creates the missing namespace chain.

For example:

configure_log_context("my_app.db.pool")

creates:

<root>
   |
   v
my_app
   |
   v
my_app.db
   |
   v
my_app.db.pool

Only the leaf context receives the explicit components passed to configure_log_context().

Intermediate contexts are created with no local sink, event policy, payload processor, or error handling policy.

That means they inherit runtime infrastructure from their parent and have no local event policy.

Updating existing contexts

configure_log_context() is an upsert operation.

If the namespace already exists, the function updates only explicitly supplied components:

configure_log_context(
    "my_app.db",
    event_policy=new_policy,
)

This changes the local event policy for my_app.db and leaves the existing local sink, payload processor, and error handling policy unchanged.

Passing None does not reset a component through configure_log_context().

To remove a local component, use the context reset method directly:

ctx.reset_event_policy()
ctx.reset_log_sink()
ctx.reset_payload_processor()
ctx.reset_log_error_handling_policy()

Wiring lock

Package-level sink and context operations are coordinated by a single wiring lock:

_log_context_wiring_lock = threading.RLock()

Public functions that read or change package-level registries use this lock.

This keeps operations such as sink registration, context configuration, sink closing, context reset, and full logger reset from interleaving in unsafe ways.

The individual registries also have their own locks. The package-level wiring lock coordinates operations that involve both registries.

Public facade

The package exposes a small facade over the registries.

Sink functions:

configure_log_sink(...)
get_log_sink(name)
get_configured_log_sink_names()
has_configured_log_sinks()
close_log_sink(name)

Context functions:

get_root_log_context()
get_log_context(namespace)
configure_log_context(namespace, ...)
get_log_context_namespaces()
has_log_context(namespace)
reset_log_contexts()

Full reset:

reset_logger()

These functions are the package-level configuration layer. They are convenient, but they are not the only way to use the logger. Direct LogContext construction remains possible.

Name validation

Package-level sink names and context namespaces are validated before registry access.

Sink names must match:

^[A-Za-z][A-Za-z0-9_-]*$

Context namespaces must match:

^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*$

This gives registered objects stable, predictable names.

The root context namespace is an internal exception. It is "", but user-supplied namespaces passed to public namespace functions must be non-empty valid dotted names.

Closing sinks

close_log_sink(name) removes a package-managed sink and calls its terminator.

If the sink name is not registered, it returns False.

If the sink is registered, the function first checks whether any registered context has that sink as its local sink.

If such contexts exist, it raises LogSinkIsInUseError.

This check protects package-managed contexts from retaining local references to closed sinks.

If the sink is not locally assigned to any registered context, the registry unregisters it and calls its terminator.

On successful close, the function returns True.

Resetting contexts

reset_log_contexts() clears all package-managed non-root contexts.

After it runs, the context registry contains only the root context:

<root>

It does not reset the sink registry.

It does not close sinks.

It does not affect LogContext instances created manually outside the package-level registry.

This operation is useful when application code wants to discard namespace configuration while keeping registered sinks available.

Resetting the whole logger

reset_logger() resets package-level logger state.

It runs under the wiring lock and performs two steps:

1. Reset the sink registry.
2. Run bootstrap again.

In code shape:

_log_sink_registry.reset()
_log_sink_registry, _log_context_registry = _bootstrap()

Resetting the sink registry clears registered sinks and calls their terminators in reverse registration order.

Then bootstrap creates a fresh sink registry, a fresh default "stderr" sink, a fresh root context, and a fresh context registry.

If closing any previous sink fails, LogSinkCloseError is raised and bootstrap is not completed.

Last-resort internal errors

Bootstrap has a minimal fallback error path.

If the default logger setup fails, _bootstrap() calls:

_log_internal_error("logger bootstrap failed", exc)

This helper writes directly to stderr using print().

It does not depend on the logger itself because the logger may not exist yet.

The same principle appears in other internal failure paths where the logging infrastructure cannot rely on itself to report its own failure.

Direct usage and bootstrap

Bootstrap affects package-level registries only.

It does not prevent direct usage:

ctx = LogContext(
    namespace="local.component",
    log_sink=sink,
    payload_processor=LogPayloadProcessor(),
)

Such a context is outside the package-level context registry unless it is created through configure_log_context().

It is not listed by get_log_context_namespaces().

It is not removed by reset_log_contexts().

Its sink cleanup is owned by the code that created the sink, unless that sink is separately package-managed.

Design summary

Package bootstrap gives the logger an immediately usable default setup.

The default setup is:

registered sink "stderr" -> StreamLogSink
root context ""          -> default sink + LogPayloadProcessor()

Package-level registries provide named sink and context management.

The wiring lock coordinates operations that involve registry state.

reset_log_contexts() clears namespace configuration but keeps sinks.

reset_logger() closes registered sinks and recreates the default root setup.

Direct component usage remains possible outside the package-level bootstrap machinery.