Skip to content

feat(smartpredictor): add structured logging on the shapash.smartpredictor logger - #742

Open
immu4989 wants to merge 1 commit into
MAIF:masterfrom
immu4989:feature/smartpredictor-structured-logging
Open

feat(smartpredictor): add structured logging on the shapash.smartpredictor logger#742
immu4989 wants to merge 1 commit into
MAIF:masterfrom
immu4989:feature/smartpredictor-structured-logging

Conversation

@immu4989

Copy link
Copy Markdown
Contributor

Description

This PR implements Gap A of #734 — structured logging at the SmartPredictor prediction boundary so production deployments can observe input shape, method timing, and error context, correlated to the deployed predictor version via the schema fingerprint introduced in #722.

Gap B (schema-drift detection with an in-pickle schema_distribution attribute, no skrub dependency, per @guillaume-vignal's decision on #734) will follow in a separate PR.

What ships:

  • Module-level logging.getLogger("shapash.smartpredictor") logger. No new dependencies — stdlib only. Silent under Python's default logging config; users opt in via standard logging.config.
  • New _instrument(operation) decorator that emits:
    • DEBUG on enter,
    • INFO on successful exit with elapsed_ms,
    • ERROR on unhandled exception (via logger.exception, so the traceback is attached).
  • Every record carries extra={"schema_fingerprint": ..., "elapsed_ms": ...} so downstream aggregators (Datadog / CloudWatch / OpenTelemetry) can correlate log records to the deployed predictor version — matching the fingerprint written by save() into the manifest sidecar (feat(smartpredictor): add version/schema manifest sidecar to save/load #722).
  • Decorator applied to five prediction-path methods: add_input, predict_proba, predict, compute_contributions, summarize. add_input also emits an extra DEBUG record with the incoming x's shape.
  • New private helper SmartPredictor._get_schema_fingerprint() lazily caches the fingerprint on _schema_fingerprint_cache. Backward compatible with pickles that predate the attribute (getattr(..., None) fallback).

Sample log output with logging.basicConfig(level=logging.INFO):

DEBUG shapash.smartpredictor: add_input enter
DEBUG shapash.smartpredictor: add_input x shape=(10000, 42)
INFO  shapash.smartpredictor: add_input completed elapsed_ms=13.4
INFO  shapash.smartpredictor: predict_proba completed elapsed_ms=182.7
ERROR shapash.smartpredictor: compute_contributions failed elapsed_ms=27.1
Traceback (most recent call last):
  ...

Fixes #734 (Gap A only).

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

6 new tests in TestSmartPredictorLogging in tests/unit_tests/explainer/test_smart_predictor.py:

  • test_add_input_emits_enter_and_completed_records — confirms both boundary records land on shapash.smartpredictor when add_input is called.
  • test_completed_records_include_elapsed_ms_and_fingerprint — asserts record.elapsed_ms is a non-negative float and record.schema_fingerprint starts with "sha256:" on every successful-exit record.
  • test_error_path_emits_failed_record — feeds a malformed x to force check_dataset_features to raise, then asserts at least one ERROR-level record with "add_input failed" message is emitted, still carrying elapsed_ms + schema_fingerprint.
  • test_logging_disable_silences_records — calls logging.disable(CRITICAL) and uses TestCase.assertNoLogs to confirm nothing is emitted, so applications that opt out via the standard API stay silent.
  • test_get_schema_fingerprint_caches — first call populates _schema_fingerprint_cache, second call returns the same cached value.
  • test_default_logging_config_is_silent — confirms no handlers are attached to shapash.smartpredictor by default (guards against accidentally hard-coding stderr emission).

Reproduce locally:

.venv/bin/python -m pytest tests/unit_tests/explainer/test_smart_predictor.py tests/unit_tests/utils/test_load_smartpredictor.py -q
# → 51 passed (45 pre-existing + 6 new logging tests)

.venv/bin/ruff format --check shapash/explainer/smart_predictor.py tests/unit_tests/explainer/test_smart_predictor.py
# → 2 files already formatted

Test Configuration:

  • OS: macOS (Darwin)
  • Python version: 3.12.13
  • Shapash version: 2.9.0 (editable install of this branch off master de85cfd)

Other library versions: pandas 3.0.3, numpy 2.4.4, catboost 1.2.10, pytest 9.0.3.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard to understand areas — docstring on the _instrument decorator explains the emission contract, docstring on _get_schema_fingerprint explains the lazy-cache and backward-compat behavior
  • I have made corresponding changes to the documentation — no user-facing API change; the new logger name is documented in the decorator docstring
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules — N/A, no dependent changes

Notes

  • Logger name. Went with shapash.smartpredictor as proposed in the issue — one logger, no sub-loggers. Happy to change to a different namespacing (e.g. shapash.smart_predictor matching the module path) if you'd prefer.
  • Silent by default. Under Python's default logging config, no records are visible to the user; existing shapash callers see no behavior change. Applications that want observability configure the logger explicitly.
  • Fingerprint caching. The _schema_fingerprint_cache attribute is populated on first _get_schema_fingerprint() call and persists across method calls. Pickles that predate this PR don't have the attribute; the getattr fallback recomputes on first use. Assumes the schema attributes (features_dict, features_types, columns_dict, label_dict) aren't mutated after __init__ — matches the current design intent.
  • Gap B follow-up. Per the shape settled on SmartPredictor production-hardening continued: structured logging and schema-drift detection (after #722) #734: schema_distribution as a new SmartPredictor attribute (in-pickle, not sidecar), lightweight self-contained implementation, no skrub dependency. Will open once Gap A lands.

…ictor logger

Implements Gap A of MAIF#734: structured logging at the SmartPredictor prediction
boundary so production deployments can observe input shape, method timing, and
error context, correlated to the deployed predictor version via the schema
fingerprint from MAIF#722.

- New module-level `logging.getLogger("shapash.smartpredictor")` logger. No new
  dependencies (stdlib only). Silent under Python's default logging config; users
  configure levels via standard `logging.config`.
- `_instrument(operation)` decorator emits DEBUG on enter, INFO on successful
  exit with `elapsed_ms`, ERROR on unhandled exception (with `logger.exception`
  attaching the traceback). Every record carries the predictor's schema
  fingerprint in `extra={"schema_fingerprint": ...}` so downstream aggregators
  (Datadog / CloudWatch / OpenTelemetry) can correlate to the deployed version.
- Applied to `add_input`, `predict_proba`, `predict`, `compute_contributions`,
  and `summarize`. `add_input` also emits an extra DEBUG record with the
  incoming `x`'s shape when provided.
- `SmartPredictor._get_schema_fingerprint()` helper lazily caches the fingerprint
  on `_schema_fingerprint_cache`, backward compatible with pickles that predate
  the attribute (getattr with `None` fallback).

Tests: 6 new tests in TestSmartPredictorLogging covering enter/completed
records, extras (`elapsed_ms` + `schema_fingerprint`), the error path,
`logging.disable(...)` silencing, the fingerprint cache behavior, and the
default-config "no handlers attached" claim. Uses `unittest.TestCase.assertLogs`
and `assertNoLogs` (Python 3.10+) — no new test dependencies.

All 51 SmartPredictor + load_smartpredictor tests pass. Ruff format clean.
Type hints on all new code per the convention established on MAIF#711 / MAIF#722.

Refs MAIF#734 (Gap A of two; Gap B — schema-drift detection with in-pickle
`schema_distribution` — will follow in a separate PR).
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.

SmartPredictor production-hardening continued: structured logging and schema-drift detection (after #722)

1 participant