feat: add --debug flag and DATAPILOT_DEBUG env var for verbose logging - #111
Merged
Conversation
…gging API failures were reported as a one-line summary such as `Error in uploading the manifest.` with no way to see why. `APIClient` already recorded the HTTP status and error body via `logger.debug`, but `dbt/cli/cli.py` pinned the root logger at `INFO` at import time, so every `DEBUG` record was discarded and there was no flag or env var to change it. Verbose output is now opt-in two ways, both resolving to the same click parameter: the `--debug` flag, and the `DATAPILOT_DEBUG` environment variable for CI/CD pipelines where the command line is generated and hard to edit. * Add `datapilot/utils/logging_utils.py` with `configure_logging()`, `is_debug_enabled()` and `redact_url()`. * Add a `debug_option` decorator and wire it into `dbt project-health` and `dbt onboard`. * Replace the import-time `logging.basicConfig(level=logging.INFO)` in `dbt/cli/cli.py` and `mcp.py` with `configure_logging()`, so `DATAPILOT_DEBUG` is honoured even on paths that never reach a command callback. * Debug is sticky, so an import-time call at `INFO` cannot undo `--debug`. * `configure_logging()` only installs a handler when the root logger has none, leaving handlers owned by embedding applications intact. Redact credentials, since debug output is meant to be shared with support: * Presigned upload URLs carry an AWS key and signature in the query string. `redact_url()` strips it in both the `put()` request log and the `Received signed URL` log, keeping the object path. * Hold `urllib3` at `INFO` in debug mode; it logs each request line verbatim, presigned query string included. * Log the `GET` request params instead, which identify the integration id, environment and file type without exposing secrets. Verified against `api.myaltimate.com`: the API token never appears in debug output, and a successful upload logs no `AWSAccessKeyId` or `Signature`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (11 files)
Notes: Credential redaction is complete at both signed-URL log sites ( Reviewed by glm-5.2 · Input: 38.5K · Output: 11K · Cached: 177.2K |
mdesmet
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A customer hit
Error in uploading the manifest.and had no way to learn anything more — theyasked whether there was "a verbose/debug flag, or logs that I could use to get more character on
the error."
There wasn't.
APIClientalready records the HTTP status and the API's error body, but it does sovia
logger.debug, anddbt/cli/cli.pypinned the root logger atINFOat import time. EveryDEBUGrecord was thrown away, and no flag or environment variable could change that.The backend was answering perfectly clearly the whole time. Reproduced against
api.myaltimate.com:The CLI collapsed that into eight words.
Change
Verbose output is now opt-in two ways, both resolving to the same click parameter:
Before / after on the same failing command:
Implementation notes
datapilot/utils/logging_utils.py—configure_logging(),is_debug_enabled(),redact_url().logging.basicConfig(level=logging.INFO)indbt/cli/cli.pyandmcp.pywithconfigure_logging(), soDATAPILOT_DEBUGis honoured even on paths that neverreach a command callback.
INFOcannot undo a--debugflag.configure_logging()installs a handler only when the root logger has none, and otherwise justsets the level. An earlier draft used
basicConfig(force=True); a test caught it wiping pytest'scaploghandler, which would equally have clobbered handlers in any app embedding datapilot.Credential redaction
Debug output exists to be pasted into support tickets, so it must be safe to share. Enabling it
previously printed the presigned S3 URL in full —
AWSAccessKeyId=...&Signature=...— four timesper run, from our own
put()log and fromurllib3, which logs each request line verbatim.redact_url()strips the query string in both places, keeping the object path(
.../manifest.json?<redacted>).urllib3is held atINFOwhenever we go toDEBUG.APIClient.get()now logs its requestparams — the integration id, environment and file type, with no secrets. This is the line that
actually pinpoints the failure above.
Verified on a successful upload against production:
AWSAccessKeyId0 occurrences,Signature=0, API token 0.
Testing
137 passed(was107onmain). 30 new tests acrosstests/utils/test_logging_utils.py,tests/cli/test_debug_option.pyandtests/clients/altimate/test_client_redaction.py, coveringenv-var parsing, flag/env precedence, stickiness, CLI-level log-level assertions, and
credential-redaction regressions.
ruffandblackclean on all changed files.api.myaltimate.comwith the built package installedinto a clean venv: default (unchanged output),
DATAPILOT_DEBUG=1,--debug, andDATAPILOT_DEBUG=0(stays quiet).Not in this PR
No
CHANGELOG.rstentry or version bump, matching the last three releases where those land in aseparate
chore: bump versioncommit.Two related issues found while reproducing this, both worth separate PRs:
dbt-adapters>= 1.24.0 is installed.dbt-adapters1.24.0 changed its bundledfunctionmaterialization tosupported_languages=['sql', 'python', 'javascript'], but the vendoredSupportedLanguageenum only allows
python/sql, soload_manifestfails before any upload happens.This is not gated on the dbt-core version: dbt-core 1.9.10 parses fine with
dbt-adapters1.23.0 and fails with 1.24.5. Verified boundary by inspecting everydbt-adapterswheel from 1.16.0 to 1.24.5.Worth noting the enum is not a deliberate restriction — it is generated from dbt's own
published schema, and
https://schemas.getdbt.com/dbt/manifest/v12.jsonstill declares['python', 'sql']. dbt now emitsjavascriptin a manifest that still identifies itselfas v12, so upstream has drifted from its own published schema.
disabledstrip-and-retry fallback added in fix: resilient manifest parsing for disabled field and unknown versions #95 can never succeed._strip_unused_fieldsremovesdisabled, butManifestV12.disabledis declaredField(...)— required. Feeding a known-good manifest through_strip_unused_fieldsfails with
disabled Field required, so the fallback turns every parse failure into amisleading two-error message that hides the real cause.
🤖 Generated with Claude Code