Skip to content

feat: add logging reset support#808

Open
nabinchha wants to merge 2 commits into
mainfrom
codex/805-reset-logging
Open

feat: add logging reset support#808
nabinchha wants to merge 2 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
@nabinchha nabinchha deployed to agentic-ci July 7, 2026 21:11 — with GitHub Actions Active
@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 adds resettable Data Designer logging. The main changes are:

  • Managed stream and file handler types for Data Designer logging.
  • Handler-state based is_logging_configured() detection.
  • New reset_logging() API that removes managed handlers.
  • Per-construction logging and default-settings initialization.
  • Updated logging tests and a new Fern logging concept page.

Confidence Score: 4/5

The logging initialization path needs a fix for mixed opt-out and default constructions.

  • A default DataDesigner() construction can run after an earlier opt-out construction.
  • In that state, app-managed root handlers do not count as Data Designer logging.
  • The later default construction can replace the application's logging handler.

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

Important Files Changed

Filename Overview
packages/data-designer-config/src/data_designer/logging.py Adds managed handler classes, observable configured-state detection, and reset logic for Data Designer-owned handlers.
packages/data-designer/src/data_designer/interface/data_designer.py Moves logging and default-settings setup into each DataDesigner construction, which enables reset support but loses a prior opt-out when a later default construction runs.
packages/data-designer-config/tests/test_logging.py Adds coverage for managed handler detection, reset behavior, foreign handler preservation after configuration, and file handler closure.
packages/data-designer/tests/interface/test_data_designer.py Updates interface tests for per-instance logging setup, opt-out behavior, preconfigured logging, and repeated construction.
fern/versions/latest/pages/concepts/logging.mdx Documents automatic logging, custom application logging, and reset behavior.
fern/versions/latest.yml Adds the Logging concept page to the latest documentation navigation.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/data-designer/src/data_designer/interface/data_designer.py:151-152
**Opt-Out State Is Lost**

When an embedding app installs its own root handler, constructs `DataDesigner(auto_configure_logging=False)`, and later constructs a default `DataDesigner()`, `is_logging_configured()` is still false because app handlers are not managed handlers. This path then calls `configure_logging()`, which clears the root handlers and redirects logging away from the application's handler.

Reviews (1): Last reviewed commit: "fix: finalize logging reset cleanup" | Re-trigger Greptile

Comment on lines +151 to +152
if auto_configure_logging and not is_logging_configured():
configure_logging()

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 Opt-Out State Is Lost

When an embedding app installs its own root handler, constructs DataDesigner(auto_configure_logging=False), and later constructs a default DataDesigner(), is_logging_configured() is still false because app handlers are not managed handlers. This path then calls configure_logging(), which clears the root handlers and redirects logging away from the application's handler.

Context Used: Do not suggest defensive coding patterns such as a... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer/src/data_designer/interface/data_designer.py
Line: 151-152

Comment:
**Opt-Out State Is Lost**

When an embedding app installs its own root handler, constructs `DataDesigner(auto_configure_logging=False)`, and later constructs a default `DataDesigner()`, `is_logging_configured()` is still false because app handlers are not managed handlers. This path then calls `configure_logging()`, which clears the root handlers and redirects logging away from the application's handler.

**Context Used:** Do not suggest defensive coding patterns such as a... ([source](greptile.json))

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for checking this. This behavior is intentional and is the first-construction-wins footgun tracked by #805: auto_configure_logging is per instance, not a sticky process-wide opt-out. The first construction with False leaves application handlers untouched; a later default construction explicitly opts back into Data Designer logging. Applications that own logging should pass False to each DataDesigner instance. The Fern page already documents this, and the PR description now calls it out explicitly. Keeping a sticky opt-out would recreate the behavior this issue removes.

if isinstance(handler, DataDesignerManagedHandler):
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(logging.WARNING)

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.

I think reset_logging() might surprise apps that own their logging: if there are no managed handlers (e.g. only a basicConfig handler), it still forces the root level to WARNING, so an app at INFO silently loses its INFO logs even though its handlers survive. Since the managed-handler check is right there, maybe early-return when nothing was removed? All the new tests still pass with that guard. Both Claude Code and Codex flagged this one independently.

@@ -1599,23 +1564,37 @@ def test_init_auto_configure_logging_false_preserves_root_handlers(


def test_init_preserves_preconfigured_logging(

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.

the old version of this test restored the logger level in a finally; now it exits with a real DEBUG-config handler on root and the next tests inherit it (works today only because the next test happens to start with reset_logging()). wdyt about an autouse fixture that snapshots/restores root handlers + levels? that would also let the other tests drop their leading reset_logging() calls.

model_providers=stub_model_providers,
secret_resolver=PlaintextResolver(),
managed_assets_path=stub_managed_assets_path,
auto_configure_logging=False,

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.

nit: this flag is load-bearing (without it configure_logging() clears root handlers and detaches caplog's capture handler when the test runs alone) - might be nice to leave a one-line comment so the next caplog test doesn't trip on the same thing

@andreatgretel

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

@andreatgretel

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

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)

2 participants