Skip to content

feat: add logging reset support#808

Merged
nabinchha merged 9 commits into
mainfrom
codex/805-reset-logging
Jul 13, 2026
Merged

feat: add logging reset support#808
nabinchha merged 9 commits into
mainfrom
codex/805-reset-logging

Conversation

@nabinchha

@nabinchha nabinchha commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

📋 Summary

Replace process-global runtime initialization latches with observable logging state and idempotent per-construction setup. Add a public reset_logging() API so embedding applications can surgically undo Data Designer logging without disturbing foreign handlers; missing default model settings are re-resolved safely on each construction.

🔗 Related Issue

Closes #805

🔄 Changes

  • Mark Data Designer-installed stream and file handlers with public managed-handler types.
  • Derive is_logging_configured() from the handlers currently attached to the root logger.
  • Add reset_logging() to remove and close managed handlers, preserve foreign handlers, and restore default root and data_designer levels.
  • Remove the logging and interface runtime latches so auto_configure_logging is evaluated for each DataDesigner construction. An explicit False applies only to that instance; a later default construction intentionally opts back into Data Designer logging.
  • Re-run resolve_seed_default_model_settings() on each construction; the operation remains idempotent and writes only missing defaults.
  • Cover observable state, handler cleanup, preconfigured logging, opt-out behavior, and repeated construction without private-global patching.
  • Add a Fern Logging concept page documenting custom configuration, application integration, and reset behavior.

🔍 Attention Areas

⚠️ Reviewers: Please pay special attention to the following:

🧪 Testing

  • make test equivalent passes: config (627), engine (2,172), interface (1,037 passed, 1 skipped)
  • Unit tests added/updated
  • E2E tests added/updated (N/A — runtime initialization and logging unit behavior)
  • Ruff lint and formatting checks pass
  • Fern validation passes with zero errors

✅ Checklist

  • Follows commit message conventions
  • Commits are signed off (DCO)
  • Architecture docs updated (N/A — no architecture contract changed; Fern user documentation added)

Description updated with AI

nabinchha added 2 commits July 7, 2026 14:41
Derive logging state from managed handlers and remove the interface runtime latch so automatic configuration is evaluated per instance.

Add reset_logging(), regression coverage, and user documentation.

Closes #805

Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Close Data Designer-managed handlers during reset so file resources are released.

Inline idempotent runtime setup in DataDesigner and verify behavior through the public constructor.

Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
@nabinchha
nabinchha requested a review from a team as a code owner July 7, 2026 21:10
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Fern preview: https://nvidia-preview-pr-808.docs.buildwithfern.com/nemo/datadesigner

Fern previews include the docs-website version archive with PR changes synced into latest. Notebook tutorials are rendered without execution outputs in previews.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR #808 — feat: add logging reset support

Author: nabinchha (Nabin Mulepati) · Base: main · Head: codex/805-reset-logging
Size: +196 / −91 across 6 files · Closes #805

Summary

This PR replaces process-global boolean latches (_logging_configured,
_interface_runtime_initialized) with observable state derived from the root
logger's attached handlers, and adds a public reset_logging() API. Key moves:

  • Introduces marker mixin DataDesignerManagedHandler plus concrete
    DataDesignerStreamHandler / DataDesignerFileHandler, so DD-installed
    handlers are identifiable by type.
  • is_logging_configured() now returns True iff a managed handler is present
    on the root logger (instead of reading a module-global flag).
  • reset_logging() detaches + closes managed handlers, preserves foreign
    handlers, and restores root → WARNING and data_designerNOTSET.
  • Removes the interface runtime latch; DataDesigner.__init__ now runs the
    logging check and resolve_seed_default_model_settings() on every
    construction.
  • Adds a Fern "Logging" concept page and comprehensive unit tests.

This is a clean, well-motivated design. Deriving state from observable reality
rather than a shadow flag eliminates the class of bug where the flag and the
actual logger state diverge (e.g. a caller clearing handlers). The change is
backward compatible at the public-API level (configure_logging,
is_logging_configured, auto_configure_logging all preserved).

Findings

Correctness

  • reset_logging() closing stream handlers is safe (verified).
    handler.close() is called on managed handlers. logging.StreamHandler.close()
    does not close its underlying stream, so a DataDesignerStreamHandler
    wrapping sys.stderr will not close sys.stderr — only FileHandler.close()
    closes its file. Good; the test test_reset_logging_closes_managed_file_handlers
    correctly asserts file_handler.stream is None for the file case only.

  • Marker-mixin MRO is sound. DataDesignerManagedHandler defines no
    __init__, so for DataDesignerStreamHandler(DataDesignerManagedHandler, logging.StreamHandler) the MRO resolves construction to
    StreamHandler.__init__ with no cooperative-super hazard. isinstance(..., DataDesignerManagedHandler) works as intended.

  • Behavioral change: resolve_seed_default_model_settings() now runs on
    every DataDesigner() construction
    (previously once per process via the
    latch). This function is idempotent — it only writes default config files /
    creates the managed-assets dir when they are absent (see
    default_model_settings.py:97), so repeated calls incur a few Path.exists()
    stat calls and nothing more. No correctness concern, minor and acceptable
    cost. Worth a one-line mention in the PR body since it is a deliberate
    semantics change, not just a refactor.

  • Asymmetry in reset_logging() (documented, intentional). Reset restores
    levels but does not re-raise the noisy third-party loggers that
    configure_logging quieted (httpx, matplotlib, mcp stay at WARNING),
    and does not restore pre-existing handlers/levels. This is explicitly called
    out in both the docstring and the MDX page, which is the right call — but it
    does mean configure_logging()reset_logging() is not a perfect
    round-trip. The docs steer users to auto_configure_logging=False for the
    preserve-my-config use case, which is the correct guidance.

Conventions / Style

  • Follows project conventions well: from __future__ import annotations added
    to the test module, modern type hints, absolute imports, SPDX headers intact,
    new handler classes fully annotated.
  • Test functions gained proper type annotations (-> None, typed fixtures) —
    consistent with the styleguide's "typed code" invariant.
  • Import direction respected: logging.py lives in data-designer-config
    (lowest layer); the interface package consumes it. No reverse imports.

Test Coverage

  • Strong. New tests cover: managed-handler detection, handler-cleared →
    is_logging_configured() False, level restoration, foreign-handler
    preservation, idempotency, and file-handler closing. The interface tests were
    rewritten to exercise real DataDesigner construction (per-instance logging
    and per-instance seed resolution) rather than patching the removed private
    helper — a genuine improvement in test fidelity.
  • Test-isolation fragility (pre-existing, slightly amplified). These tests
    mutate process-global root-logger state. Several rely on calling
    reset_logging() / configure_logging() at the top of the test rather than a
    fixture, so ordering-dependent handler pollution across the module is
    possible. test_init_resolves_seed_default_model_settings_per_instance does
    not reset logging first; it passes because it only asserts on the patched
    resolve call count, but it leaves managed handlers attached for whatever
    runs next. Consider an autouse fixture that snapshots/restores root handlers
    and levels around each logging-touching test. Non-blocking.
  • The added auto_configure_logging=False in
    test_create_dataset_warns_for_unhandled_transform_files is a reasonable way
    to avoid handler pollution from that e2e test; good defensive change.

Documentation

  • The new fern/versions/latest/pages/concepts/logging.mdx is accurate: the
    import paths (from data_designer.interface import DataDesigner,
    from data_designer.logging import LoggingConfig, configure_logging, reset_logging) match the actual module layout, and the described default
    behavior (root console handler + data_designer at INFO) matches
    LoggingConfig.default(). Nav entry added to latest.yml. Clear treatment of
    the round-trip caveat.

Minor / Optional

  • logging.py has no __all__; the new public symbols (reset_logging,
    DataDesignerManagedHandler, etc.) are exported implicitly. Pre-existing
    pattern for this module, so not introduced here — but if this module is
    considered public surface, an __all__ would make the contract explicit.
  • is_logging_configured()'s line is long but within the project's configured
    line length (the codebase uses wide lines elsewhere); assuming ruff passes as
    the PR states, no action.

Structural Impact

(graphify, 2.4s)

Risk: MEDIUM (high-connectivity entity (DataDesigner, 86 deps))

  • 4 Python files, 43 AST entities, 2/86 clusters

High-Connectivity Changes

  • DataDesigner (86 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Connect to a configured MCP provider and return the names of its available tools (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Async wrapper for creating a dataset without blocking the caller's event loop. (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Create an experimental composite workflow. Workflow chaining is experim (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Get a dict of ModelFacade instances for custom column development. Use (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Get the default model configurations. Returns: List of defa (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Probe every model and MCP tool referenced by the configuration. Runs th (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • Generate preview dataset for fast iteration on your Data Designer configuration. (40 deps) in packages/data-designer/src/data_designer/interface/data_designer.py
  • +23 more

Cross-Package Dependencies

  • DataDesigner (interface) --uses--> DatasetProfilerResults (config)
  • DataDesigner (interface) --uses--> DataDesignerConfigBuilder (config)
  • DataDesigner (interface) --uses--> DataDesignerConfig (config)
  • DataDesigner (interface) --uses--> DataDesignerInterface (config)
  • DataDesigner (interface) --uses--> ModelConfig (config)
  • DataDesigner (interface) --uses--> ModelProvider (config)
  • +649 more

Reviewer note on blast radius: The MEDIUM risk stems from touching
DataDesigner.__init__, which is the highest-connectivity entity in the graph.
The change to that constructor is narrow (inlining the two initialization calls
that the removed latch previously guarded) and preserves the public signature,
including the auto_configure_logging keyword. No cross-package dependency
edges are added or removed — interface → config direction is intact. The
backward-compatibility surface is therefore limited to the one documented
semantic change (per-construction re-evaluation of logging + seed settings),
which the new interface tests explicitly cover.

Verdict

Approve with minor, non-blocking suggestions. This is a solid refactor that
replaces fragile global latches with observable, testable state and adds a
useful, well-documented public API. Correctness of the risky bits (stream-handler
closing, MRO, idempotent seed resolution) checks out. The only items worth
addressing are optional: (1) note the per-construction re-evaluation of
resolve_seed_default_model_settings() in the PR description as an intentional
semantics change, and (2) consider an autouse fixture to harden logging-test
isolation. Neither blocks merge.

Note: make test was not re-run in this review environment (no configured
venv); the assessment is based on static analysis of the diff and surrounding
source. The PR states config/engine/interface suites and ruff/Fern checks pass.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces process-global boolean latches (_logging_configured, _interface_runtime_initialized) with observable, handler-based state detection and a new public reset_logging() API. The DataDesignerManagedHandler marker mixin lets is_logging_configured() inspect root-logger handler state directly, making the design testable without private-global patching.

  • configure_logging() / reset_logging(): handlers are now tagged at creation (DataDesignerStreamHandler, DataDesignerFileHandler), allowing reset_logging() to surgically remove only managed handlers and leave foreign handlers untouched.
  • DataDesigner.__init__: per-construction logging setup replaces the one-shot initializer; resolve_seed_default_model_settings() is also called per-construction (idempotent by design). Tests drop monkeypatch in favour of real reset_logging() calls, and auto_configure_logging=False is added where caplog capture handlers must be preserved.
  • Fern docs: new logging.mdx concept page covers custom config, application-integration opt-out, and reset semantics.

Confidence Score: 5/5

Safe to merge. The observable-state approach is correct, reset_logging correctly guards foreign handlers, and tests cover the full behavioural surface without private-global patching.

The refactor replaces two process-global booleans with handler-presence detection, which is sound: is_logging_configured() reads root-logger state directly, reset_logging() early-exits when no managed handlers are present so it never touches foreign log levels, and the DataDesignerManagedHandler MRO is correct for both StreamHandler and FileHandler subclasses. The per-construction resolve_seed_default_model_settings() call is claimed idempotent and confirmed by the new test. All previous private-global monkeypatching is replaced with real reset_logging() calls in tests, making the test suite more faithful to production behaviour.

packages/data-designer-config/src/data_designer/logging.py — the configure_logging() change from handlers.clear() to remove+close on every root handler was flagged in a prior thread; confirm team consensus on the foreign-handler close semantics before merging.

Important Files Changed

Filename Overview
packages/data-designer-config/src/data_designer/logging.py Introduces DataDesignerManagedHandler marker mixin and two concrete handler subclasses; replaces process-global bool with handler-presence detection; adds reset_logging(). configure_logging() now closes all root handlers (including foreign ones) rather than merely detaching them — a behaviour change that was flagged in a prior review thread.
packages/data-designer/src/data_designer/interface/data_designer.py Removes _initialize_interface_runtime one-shot function; inlines per-construction configure_logging guard and resolve_seed_default_model_settings call into init. Logic is correct and matches documented per-instance semantics.
packages/data-designer-config/tests/test_logging.py Replaces monkeypatch-of-private-global tests with real-state tests using reset_logging(); adds comprehensive coverage of reset semantics, foreign-handler preservation, idempotency, and file-handler close behaviour.
packages/data-designer/tests/interface/test_data_designer.py Old monkeypatched _initialize_interface_runtime tests removed; new tests verify per-instance logging, handler stability, and per-construction resolve_seed_default_model_settings. Adds auto_configure_logging=False to the caplog-dependent test to preserve pytest's capture handler.
fern/versions/latest/pages/concepts/logging.mdx New Fern concept page documenting configure_logging, auto_configure_logging opt-out, and reset_logging. Content accurately reflects implementation semantics.
fern/versions/latest.yml Adds Logging navigation entry pointing to the new logging.mdx concept page. Trivial config change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App
    participant DD as DataDesigner.__init__
    participant IL as is_logging_configured
    participant CL as configure_logging
    participant RL as reset_logging
    participant Root as Root Logger

    App->>DD: DataDesigner()
    DD->>IL: check handlers
    IL->>Root: any DataDesignerManagedHandler?
    Root-->>IL: False
    IL-->>DD: False
    DD->>CL: configure_logging()
    CL->>Root: remove+close all existing handlers
    CL->>Root: addHandler DataDesignerStreamHandler
    CL->>Root: setLevel INFO

    App->>DD: DataDesigner() second construction
    DD->>IL: check handlers
    Root-->>IL: True managed handler present
    IL-->>DD: True
    DD-->>App: skip configure_logging idempotent

    App->>RL: reset_logging()
    RL->>Root: find managed handlers
    Root-->>RL: DataDesignerStreamHandler list
    RL->>Root: removeHandler+close each managed handler
    RL->>Root: setLevel WARNING
    RL->>Root: data_designer setLevel NOTSET

    App->>DD: DataDesigner() after reset
    DD->>IL: check handlers
    Root-->>IL: False no managed handlers
    IL-->>DD: False
    DD->>CL: configure_logging() again
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App
    participant DD as DataDesigner.__init__
    participant IL as is_logging_configured
    participant CL as configure_logging
    participant RL as reset_logging
    participant Root as Root Logger

    App->>DD: DataDesigner()
    DD->>IL: check handlers
    IL->>Root: any DataDesignerManagedHandler?
    Root-->>IL: False
    IL-->>DD: False
    DD->>CL: configure_logging()
    CL->>Root: remove+close all existing handlers
    CL->>Root: addHandler DataDesignerStreamHandler
    CL->>Root: setLevel INFO

    App->>DD: DataDesigner() second construction
    DD->>IL: check handlers
    Root-->>IL: True managed handler present
    IL-->>DD: True
    DD-->>App: skip configure_logging idempotent

    App->>RL: reset_logging()
    RL->>Root: find managed handlers
    Root-->>RL: DataDesignerStreamHandler list
    RL->>Root: removeHandler+close each managed handler
    RL->>Root: setLevel WARNING
    RL->>Root: data_designer setLevel NOTSET

    App->>DD: DataDesigner() after reset
    DD->>IL: check handlers
    Root-->>IL: False no managed handlers
    IL-->>DD: False
    DD->>CL: configure_logging() again
Loading

Reviews (8): Last reviewed commit: "Merge branch 'main' into codex/805-reset..." | Re-trigger Greptile

Comment thread packages/data-designer/src/data_designer/interface/data_designer.py
Comment thread packages/data-designer-config/src/data_designer/logging.py
Comment thread packages/data-designer/tests/interface/test_data_designer.py
Comment thread packages/data-designer/tests/interface/test_data_designer.py
@andreatnvidia

Copy link
Copy Markdown
Contributor

packages/data-designer-config/src/data_designer/logging.py:135

# Remove all handlers
root_logger.handlers.clear()

now that reset_logging() closes managed handlers, this bare clear() is the one inconsistent spot: configure_logging() twice with a file destination leaks the first open file handle, and once detached reset can never reclaim it. could reuse the same remove+close loop here

@andreatnvidia

Copy link
Copy Markdown
Contributor

packages/data-designer/src/data_designer/interface/data_designer.py:135

Automatic configuration is also skipped when
`data_designer.logging.configure_logging()` was already called.

tiny doc nit: "skipped when configure_logging() was already called" described the old latch - under the new semantics it's skipped only while a managed handler is still on the root logger (e.g. basicConfig(force=True) would remove it and a later construction re-configures). Claude Code caught this while cross-checking the docstrings against the new behavior

Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
@nabinchha

Copy link
Copy Markdown
Contributor Author

Addressed both new comments in e5a8d09:

  • configure_logging() now removes and closes each handler it replaces, with a regression test confirming a displaced file handler is closed.
  • The auto_configure_logging docstring now describes the observable handler-state behavior: automatic configuration is skipped only while a Data Designer-managed handler remains attached.

Verification: 106 targeted tests passed; Ruff lint and format checks passed.

@nabinchha

Copy link
Copy Markdown
Contributor Author

Replying to the handler lifecycle comment: addressed in e5a8d09d. configure_logging() now removes and closes every root handler it replaces, so repeated configuration cannot leave a displaced file handler open. I also added a regression test that verifies the old file handler is detached and its stream is closed.

@nabinchha

Copy link
Copy Markdown
Contributor Author

Replying to the constructor docstring comment: addressed in e5a8d09d. The docstring now says automatic configuration is skipped only while a Data Designer-managed handler remains attached to the root logger, matching the new observable-state behavior.

@nabinchha
nabinchha requested a review from andreatnvidia July 8, 2026 20:06
andreatnvidia
andreatnvidia previously approved these changes Jul 8, 2026
Resolve conflict in data_designer.py by keeping PR's inlined logging
configuration (for reset support) alongside main's OpenTelemetry
runtime initialization and _observe_create decorator.
andreatnvidia
andreatnvidia previously approved these changes Jul 13, 2026
mikeknep
mikeknep previously approved these changes Jul 13, 2026
Comment thread fern/versions/latest/pages/concepts/logging.mdx
Address review feedback by including is_logging_configured in the
documented import from data_designer.logging.
@nabinchha
nabinchha dismissed stale reviews from mikeknep and andreatnvidia via 5309c80 July 13, 2026 15:36
Comment on lines +134 to +137
# Remove and close all handlers replaced by this configuration.
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 configure_logging() closes foreign handlers it doesn't own

configure_logging() iterates over every root-logger handler and calls .close(), including handlers installed by embedding applications. The old implementation used handlers.clear(), which merely detached them from the root logger without closing the underlying resources. The new code closes any FileHandler — or StreamHandler whose stream is e.g. sys.stderr — that a third-party added. An embedding app that holds a direct reference to such a handler and continues emitting records will get a ValueError: I/O operation on closed file after configure_logging() runs. Only DataDesignerManagedHandler instances need to be closed; foreign handlers should be removed from the root logger but left open, matching the behavior reset_logging() already implements correctly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-config/src/data_designer/logging.py
Line: 134-137

Comment:
**`configure_logging()` closes foreign handlers it doesn't own**

`configure_logging()` iterates over every root-logger handler and calls `.close()`, including handlers installed by embedding applications. The old implementation used `handlers.clear()`, which merely detached them from the root logger without closing the underlying resources. The new code closes any `FileHandler` — or `StreamHandler` whose stream is e.g. `sys.stderr` — that a third-party added. An embedding app that holds a direct reference to such a handler and continues emitting records will get a `ValueError: I/O operation on closed file` after `configure_logging()` runs. Only `DataDesignerManagedHandler` instances need to be closed; foreign handlers should be removed from the root logger but left open, matching the behavior `reset_logging()` already implements correctly.

How can I resolve this? If you propose a fix, please make it concise.

@andreatnvidia

Copy link
Copy Markdown
Contributor

CI was failing in multiple PRs due to some weird issue, I just merged #814 which is supposed to fix it. Rebased to see if it helps

@nabinchha
nabinchha merged commit b859a23 into main Jul 13, 2026
62 checks passed
@nabinchha
nabinchha deleted the codex/805-reset-logging branch July 13, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cleanup: remove runtime-init globals, add public reset_logging() (follow-up to #755)

3 participants