All notable changes to this project are documented in this file.
-
Local-first trace viewer:
logquill trace <run_id> --file logs.jsonlreconstructs and prints one agent run's span tree, annotated with each span's own duration and the token/cost totals rolled up from everything nested under it. It streams the file line by line, so tracing one run out of a multi-gigabyte log costs memory proportional to that run, not the file (a gigabyte-scale test asserts this).--jsonprints the same tree as nested data instead.logquill serve --file logs.jsonl(or--db logs.sqlitefor aSQLiteTransportdatabase) runs a small local web UI — run list, a combined span-tree/waterfall view, search, and a level filter — built entirely on the stdlib (http.server,sqlite3): no new dependency, no account, nothing leaves the machine. Reading from a SQLite database never shows token/cost annotations, since that transport's fixed schema doesn't store thellmblock.logquill dev logs.jsonlfollows a file liketail -f, but live-renders the current run's span tree (colorized, screen-cleared between redraws) instead of flat lines, following whichever run is most recently active unless--run-idpins it to one.
-
Auto-instrumentation, an OpenAI Agents SDK adapter, and MCP trace propagation:
logquill.instrument.anthropic(logger)/.openai(logger)/.litellm(logger)patch the Anthropic, OpenAI, and litellm Python SDKs so every LLM call they make anywhere in the process emits alogger.llm_call(...)— no call-site changes. Each has a matching.uninstrument(), lives behind its own optional extra (logquill[instrument-anthropic],[instrument-openai],[instrument-litellm]), and is imported lazily:import logquill.instrumentnever imports a provider SDK. Streaming calls are a documented gap in this release — passed through untouched rather than partially instrumented.OpenAIAgentsAdapter(pip install logquill[openai-agents]) maps the OpenAI Agents SDK'sRunHooksthe same way the existing adapters map their frameworks: every agent activation (including a handoff's target) is its owninvoke_agentspan, andon_llm_endbecomes a real.llm_call()with token usage, soOTLPTransportexports a full run — spans, tokens, cost — with zero manual logging calls.logquill.mcp(propagate()/inbound()) propagates trace context over an MCP request's_metafield and stampsmeta.mcp.*on records logged while handling one, with no dependency on themcppackage itself. A client'srun_idrides along informationally asmeta.mcp.run_id; it never overrides the handling process's ownRunPluginrun id.- The record contract gained
meta.mcp.run_id.
-
LLM calls and OpenTelemetry export:
logger.llm_call(model=, tokens_in=, tokens_out=, cost_usd=, latency_ms=, finish_reason=)records an LLM call with its numbers in the record's first-classllmblock. A value that breaks the contract is dropped with a warning instead of raising.OTLPTransport(pip install logquill[otel]) exports agent tracing as real OpenTelemetry spans through the SDK:span()blocks, tool.action()s and LLM calls, with the ids, parents and timings the records carry, so a collector shows exactly the tree that was logged. Spans are named and attributed per the OpenTelemetry GenAI conventions (invoke_agent,execute_tool,chat, with token counts, model and finish reason).OTelLogsTransportsends every record as an OTLP log record, with the same trace and span ids so logs and spans join up.- The convention names live in one file,
logquill/semconv.py, pinned to a named release (semantic-conventions 1.44.0), because the GenAI conventions are still experimental and have already renamed attributes.OTEL_SEMCONV_STABILITY_OPT_INis honored,semconv_version="legacy"picks the older names, and old and new names are never emitted together. Prompt and completion text is opt-in only. span(name, capture_state=...)records what changed during a block asmeta.state_diff, and a tool.action()that is reopened before it succeeded getsmeta.retry_countautomatically.- The record contract gained optional
metafields for this:tool,tool_call_id,provider,agent_name,agent_id,response_model,operation, and opt-ininput_messages/output_messages. - Type-checking no longer depends on whether OpenTelemetry is installed.
-
Breaking: the record shape and the Python floor changed. See MIGRATING.md.
- logquill now requires Python 3.10 or newer and is tested on 3.10–3.14. Python 3.8 and 3.9 are end-of-life; pip on those interpreters keeps installing 1.x.
- Every record now carries
schema_version("2.0"). Nothing was renamed or removed, so a reader only breaks if it insists on exactly the five 1.x keys.parse_record()reads 1.x and 2.x records alike, labelling a 1.x record"1.0". - New reserved fields, shared with
logquillon npm: a top-levelllmblock (model,tokens_in,tokens_out,cost_usd,latency_ms,finish_reason) for LLM calls, plusmeta.retry_count,meta.state_diffandmeta.mcp.server/meta.mcp.tool. The text and logfmt formatters show thellmblock. Nothing writes these on its own yet. TamperEvidentPluginnow coversschema_versionandllmin the hash, so editing either is caught; hash chains written by 1.x still verify.- Fixed: the New Relic transport rebuilt each record from its five 1.x fields
and would have dropped
schema_versionandllm; it now copies the record. - The record format now has a machine-readable definition,
schema/record.schema.json(JSON Schema 2020-12), and a shared file of golden records,schema/golden_records.json.tests/test_contract.pychecks the schema, the golden records, the parser, and everything the logger actually writes against each other, so drift between the Python and JavaScript packages now fails a test instead of relying on a reviewer to notice.
-
Added the 1.0 features that hadn't shipped yet, plus hardening:
logger.opt(lazy=True)defers callablemetavalues until a record is really going to be emitted, so an expensiveDEBUG/TRACEargument costs nothing when the level filters the call out. A callable that raises leaves a placeholder in the record instead of crashing the caller.logger.opt(depth=N)addsmeta.caller(module,function,line,file) naming the code that logged,Nframes up, so a call made from a wrapper or decorator reports the wrapper's caller.logquill.disable(name)/logquill.enable(name)switch off a logger and everything nested under it (most specific rule wins), so a library that logs through LogQuill can be silent in its host application by default —logquill.disable(__name__)— and the application can turn it back on.- Two new formatters:
TextFormatter(a human-readable entry per record, with tracebacks on the lines below) andLogfmtFormatter(single-linekey=valueoutput; values are quoted so a record is always one line). Formatters now live in thelogquill.formatterspackage;logquill.formatterstill works. A transport'soptionsin a config file can name one:{"formatter": "text"}.logquill tailnow prints tracebacks the same wayTextFormatterdoes. parse(source, pattern, cast=...)extracts structured fields from a log file with a regex — including legacy and third-party formats — streaming line by line.parse_logfmt()reads logfmt back, andTEXT_LOG_PATTERNreadsTextFormatteroutput.AppriseAlertPlugin(pip install logquill[apprise]) sends alerts through Apprise, reaching 100+ notification services with the same deduplication and non-blocking behavior as the other alerting plugins.- Every
Loggernow flushes and closes its transports at interpreter exit (anatexithook), so a script that never callsclose()no longer loses its last queued records or a batching transport's unsent batch. Opt out withLogger(flush_at_exit=False)or"flush_at_exit": falsein config. diagnose=Trueon anyLoggermethod adds each traceback frame's local variable values. Off by default, with an explicit warning in the docs and once per process in the log: it can leak sensitive data. Captured values go throughRedactPluginandPIIRedactPluginbefore the traceback is formatted (via a new optionalPlugin.redact_localhook), and a plugin whose hook raises masks the value instead of showing it.HTTPTransport(backend="aiohttp")sends over one reused keep-alive connection, giving thehttpextra a purpose. Also,HTTPTransportnow bounds its buffer bymax_bytesas well asbatch_size, and a failed send is logged with an actionable message and the batch dropped, rather than raising into the code that logged.- Fixed: the async queue's "dropping records" warning could stay silent for the first minute after a process started, because its rate limiter compared against a monotonic clock whose zero point is arbitrary.
- Fixed: passing a malformed
exc_info(for example a forwarded dict that happens to carry a badexc_infokey) raised out of the log call. It is now ignored with a warning, like any other badmeta. - A memory-budget suite (
pytest benchmarks, its own CI job) fails the build if a log call, a level-filtered call, or a burst into a stalled sink uses materially more memory than it does today. New tests burst 30,000 records at a stalled transport under each backpressure policy and assert exactly which records survive, andhypothesiscoverage now extends to the formatters and transports.
- First stable release: bumped the
Development Statusclassifier from3 - Alphato5 - Production/Stable, matching1.0.0. - Packaging polish:
- Confirmed the
py.typedmarker ships correctly inside the built wheel (verified by installing that wheel into a fresh virtualenv and runningmypy --strictagainst a script importing the installed package, not the local checkout) and thatpyproject.toml's keywords, project URLs, and optional-dependency extras were already complete. - Fixed a real sdist-packaging bug found while verifying the above: the
sdist was bundling whatever untracked local files happened to sit in
the working tree at build time — including the local
hypothesistest cache (.hypothesis/, 145 files) and other per-machine tool state — alongside the actual source, sincehatchling's default sdist selection includes anything not explicitly.gitignored, and not every local artifact was. Replaced that with an explicit[tool.hatch.build.targets.sdist]allowlist naming exactly the paths the package needs (logquill/,tests/, docs, license,pyproject.toml), so a sdist built from any contributor's machine stays limited to actual project files regardless of what else is sitting in the working tree. - Verified
python -m buildproduces a clean wheel and sdist,twine check dist/*passes on both, and installing the built wheel into a fresh virtualenv works end to end — import, logging calls, and thelogquillCLI entry point all function against the installed package.
- Confirmed the
- Phase 9, docs, complete:
- Every public class and function across the package now has a docstring explaining what it does and why you'd reach for it — not a restatement of its name — matching the bar already set by the existing public API.
- A full API reference, generated straight from those docstrings with
pdoc (
pip install logquill[docs]), is published to GitHub Pages and rebuilt automatically on every push tomainvia.github/workflows/docs.yml. - README: a new "Kubernetes" section explains why
ConsoleTransport(stdout/stderr, captured by the node's log agent) belongs in a container instead ofFileTransport(writes to an ephemeral filesystem nothing aggregates), and how to avoid losing queued records toSIGTERMwhenasync_dispatch=Trueis combined with a container's termination grace period.
- Phase 8, CLI, complete:
logquill tail <file> [--level=] [--json] [-f/--follow] [-n/--lines]— alogquillconsole-script for tailing a JSONL log file in local dev. Human-readable output by default, colorized by level to matchConsoleTransport;--jsonprints each matching record as a raw JSON line instead.--levelfilters to that level and above;-nlimits to the last N matching records;-f/--followkeeps polling the file for newly appended records, for atail -f-style live view. A line that isn't valid JSON, or isn't a JSON object, is skipped with a warning on stderr instead of aborting the whole tail.
- Phase 7, advanced context & stdlib bridge, complete:
bind_context(**values)— acontextvars-based context manager that mergesvaluesinto everyLoggercall underneath it, through any method and any number of function calls deep, without threading them through each signature by hand. Isolated per thread/asyncio task; nested blocks merge, with the innermost value winning on key collision (an explicit call-sitemetavalue still wins over anything bound this way).current_context()reads the merged dict directly.exc_info=on everyLoggermethod (logger.error("failed", exc_info=e)) — accepts the same shapes stdlibloggingdoes (an exception instance,Truefor the exception currently being handled, or an explicit(type, value, traceback)tuple), formats a traceback intometa["stack"], and is never kept inmetaas the raw exception object, since that isn't serializable.format_exc_info()is exported directly for anything that wants the same formatting standalone.LogQuillHandler— alogging.Handlersubclass that bridges stdlibloggingcalls (including from third-party libraries) into a LogQuillLogger, so they flow through the same transports and plugin pipeline as a native.info()/.error()/... call.extra=fields land inmeta; an attachedexc_infois formatted intometa["stack"]the same way theLogger's ownexc_info=kwarg is. Level filtering still applies on top of whatever the stdlib logger/handler's own level is set to.RateLimitPlugin(max_records, per_seconds)— drops records once a key (by default(logger, level), or a customkey_func) exceedsmax_recordswithin a rolling per-key window, to cap a noisy loop without silencing the logger's other messages. Bounded bymax_keysdistinct keys tracked at once, evicting the least-recently-seen key's window to make room for a new one.
- Phase 6, async worker, shutdown & serverless safety, complete:
AsyncWorker— a bounded, in-memory queue backed by a single daemon thread, with a configurablebackpressurepolicy for what happens once that bound is hit under a sustained burst:drop_oldest(default, evicts the oldest queued item),drop_newest(discards the item that just overflowed the queue), orblock(the submitting thread waits for space instead of dropping anything). Either drop policy logs at most one warning per minute while actively dropping, not one per drop.Logger(async_dispatch=True, max_queue_size=10_000, backpressure= "drop_oldest")— moves each record's transport writes andafter_logplugin hooks onto that background thread, so.info()/.error()/... return without waiting on a transport's I/O;before_loghooks still run synchronously, since a later hook or transport needs to see their result in order.Logger.child()shares its parent's worker rather than starting a second background thread.Logger.flush(timeout=None)/await Logger.flush_async(timeout=None)— drain any queued records and flush each transport's own internal buffer (Transport.flush(), a new no-op-by-default hook; already matched byBatchingTransport's existing buffered-batch flush) without closing anything, so the logger stays usable right after.Logger.close (timeout=5.0)now drains the queue (up totimeout) before closing every transport.with_lambda(logger_or_loggers, timeout=5.0)— wraps a handler soflush()/flush_async()runs before the handler's result or exception reaches the caller, covering both sync andasync defhandlers. Flushes rather than closes, since a warm serverless container reuses the sameLogger/transports on its next invocation.with_cloud_functionandwith_azure_functionare the same decorator under a name that reads naturally at each platform's own handler definition — the flush-before-return behavior needed is identical across all three.load_config/logger_from_file/logger_from_envaccept the new"async_dispatch"/"max_queue_size"/"backpressure"config keys, mapped straight onto the matchingLoggerconstructor arguments.
LangGraphAdapter(pip install logquill[langgraph]) — corrects an overstatement in the 0.4.0 entry below: LangGraph nodes run as ordinary LangChainRunnables, soLangChainAdapteralone already captures node execution, but LangGraph also has its own checkpoint lifecycle —on_interrupt/on_resume, fired when a graph pauses on aninterrupt()call (e.g. for human review) and later resumes from a persisted checkpoint — that LangGraph dispatches only to handlers that are instances of its ownGraphCallbackHandler; a plainBaseCallbackHandlersubclass (allLangChainAdapteris) never receives them.LangGraphAdapterisLangChainAdapterplus those two, mapped to.observation ("graph_interrupted", ...)/.action("graph_resumed", ...)carryingcheckpoint_id/status/checkpoint_ns/pendingInterruptpayloads, with the event's ownrun_idasparent_span_id.pip install logquill[langgraph]pulls in a compatiblelangchain-coretransitively;langgraphis never imported unlesslogquill.adapters.langgraphis imported explicitly.
- Closed three gaps found auditing Phases 1–3 against their own written
exit criteria (each had been marked "shipped" despite this):
- Phase 1: config loading from file/env.
load_config(dict),logger_from_file(path)(JSON built in; YAML via the optionalpip install logquill[yaml]), andlogger_from_env(prefix= "LOGQUILL_")build aLoggerfrom{"name", "level", "transports": [{"type"|"class", "options"}], "plugins": [...]}. A small built-in"type"registry covers the zero-dependency transports/plugins; anything else (every cloud/SQL/NoSQL/queue transport, the alerting plugins, framework adapters, your own subclass) goes through"class"— a fully-qualified dotted path, resolved the same waylogging. config.dictConfigresolves one.{prefix}LEVELin the environment always overrides a config file's level. - Phase 2:
FileTransport(encrypt_key=...)encrypts each line withcryptography.fernet.Fernetbefore writing — worth doing here specifically because cloud transports typically already encrypt server-side, but a local log file on disk usually doesn't. Optionalpip install logquill[crypto], imported lazily. - Phase 3:
SyslogTransport— RFC 5424 messages over UDP (default) or TCP, stdlibsocketonly, no dependency. Not a batching transport, unlike the HTTP-API cloud transports: syslog is one-datagram/one- message-per-call.
- Phase 1: config loading from file/env.
- Fixed a pre-existing crash the plugin-pipeline hypothesis property test
caught during this audit, unrelated to the three gaps above: any of
Logger's message-taking methods (.info(),.error(),.action(), ...) raisedTypeError: got multiple values for argument 'message'if the caller's**metahappened to contain a key literally named"message"(and.child()/.span()had the same issue with"name") — exactly the kind of caller-crashing bug those hypothesis tests exist to catch.message/nameare now positional-only on every affected method, so ameta/fixed_metakey with that exact name now flows through as ordinary meta instead of colliding. - Phase 5, trace correlation & agentic tracing, complete:
Logger.child(name, **fixed_meta)— a namespaced logger sharing the parent's transports, with its own plugin pipeline and optional fixed context injected into every record..thought()/.action()/.observation()/.decision()—.info()withmeta.kindpre-set, for tagging agent reasoning steps.Logger.span(name), used aswith agent_log.span("call_llm"):— emits one record on exit carryingmeta.span_id/meta.duration_ms; every record logged inside the block (through any method) is automatically stamped withmeta.parent_span_id, so a full run reconstructs its exact nesting by sorting onspan_id/parent_span_id. Still emits its record, atERRORwithmeta.errorset, if the block raises — the exception itself propagates unchanged.RunPlugin— stampsmeta.run_id(generated if not given) and an incrementingmeta.step; one instance scopes one run, so concurrent runs never share a counter.TraceContextPlugin— stampsmeta.trace_idfor cross-service correlation, distinct fromrun_id. Resolves an active OpenTelemetry span's trace id first (best-effort, lazy import), then a W3Ctraceparent/AWS X-Ray/GCP trace header propagated via the newset_traceparent()/reset_traceparent()(acontextvars-based per-thread/asyncio-task mechanism), and generates a fresh id only if neither is available.LogQuillAdapterbase class +LangChainAdapter(pip install logquill[langchain]) — maps LangChain'sBaseCallbackHandlerevents onto the calls above; covers LangGraph for free, since it shares LangChain's callback system. LangChain's ownrun_id/parent_run_idare written directly ontometa.span_id/meta.parent_span_id.langchain-coreis never imported unlesslogquill.adapters.langchainis imported explicitly.
CrewAIAdapter(pip install logquill[crewai]) — a secondLogQuillAdapterimplementation, ahead of the phase schedule (CrewAI was listed as a Phase 5 follow-on, not required for that phase). Listens on CrewAI's own event bus (BaseEventListener) rather than a single callback handler; a crew kickoff and each task open/close a.span(), while agent execution, tool usage, and LLM calls become.action()/.observation()/.error()pairs withduration_ms. Correlation reads directly off CrewAI's ownevent.parent_event_id/event.started_event_id(populated by CrewAI's owncontextvars-backed scope stack) rather than tracking anything independently — the same field-renaming approachLangChainAdaptertakes with LangChain'srun_id/parent_run_id.crewaiis never imported unlesslogquill.adapters.crewaiis imported explicitly.LlamaIndexAdapter(pip install logquill[llamaindex]) — a thirdLogQuillAdapterimplementation. LlamaIndex's own instrumentation module splits into two cooperating registrations on a shared dispatcher, so this adapter holds one of each rather than being a handler itself: a span handler for LlamaIndex's own method-level calls (query(),chat(),retrieve(), ...), each becoming aspan_id/duration_msrecord withparent_span_idset for a nested call; and an event handler for named events fired within those calls, classified generically by class-name suffix (*StartEvent->.action(),*EndEvent->.observation(),*ErrorEvent->.error()) rather than enumerated one by one, so a new LlamaIndex event type needs no adapter change to show up correctly.llama-index-coreis never imported unlesslogquill.adapters.llamaindexis imported explicitly.AutoGenAdapter(pip install logquill[autogen]) — a fourthLogQuillAdapterimplementation, rounding out every framework CLAUDE.md names as a Phase 5 follow-on. Architecturally different from the other three: (Microsoft) AutoGen's actual integration point is a stdliblogging.Handlerattached toautogen_core.EVENT_LOGGER_NAME, where model clients and tools log structured event objects (not strings), so the adapter is aHandlerwhoseemit()unpacks that object rather than a callback/event-bus registration. Each event becomes a flat.action()/.observation()/.error()record; unlike the other three adapters, AutoGen's structured events carry no call-levelspan_id/parent_span_id-equivalent (onlyagent_id), so there's no span tree to reconstruct here — documented as a real limitation, not glossed over. Coversautogen-core/autogen-agentchatonly — not AG2, which forked from AutoGen and, as of its 2026 rewrite, moved onto its own event-driven architecture sharing none of this (confirmed against its source: zero references toEVENT_LOGGER_NAME); unlike LangGraph sharing LangChain's callback system, this is a genuine divergence and AG2 would need its own adapter.autogen-coreis never imported unlesslogquill.adapters.autogenis imported explicitly.
-
Plugin pipeline, Phase 4 complete:
SamplingPlugingained tail-based elevation — withtransports=set, a record that would be dropped is buffered permeta["trace_id"](configurable viatrace_key) instead of discarded outright, and if any later record in that trace reacheselevate_at(defaultERROR), the whole trace — every buffered record plus everything after — ships, flushed straight totransports. Buffering is bounded bymax_buffered_recordsandmax_traces, oldest trace evicted first. Withouttransports, behavior is unchanged from plain rate-based sampling. -
Logger.use()(and theplugins=[...]constructor list) now accepts a plain function alongside aPlugininstance — wrapped internally as an anonymousPlugin(FunctionPlugin) — so a one-offbefore_log-style transform doesn't require subclassingPluginfirst. -
PIIRedactPlugin: regex-based PII redaction overmetavalues (emails, SSNs, credit-card numbers, phone numbers), recursing through nested dicts/lists/tuples and matching regardless of which key holds the value — complementsRedactPlugin's exact-key matching. Depth- and cycle-bounded, so a circular reference or pathologically deep structure can't hang or crash the caller. An opt-inuse_presidio=Truemode (pip install logquill[presidio]) routes values through Microsoft Presidio's analyzer/anonymizer instead, for ML-based detection; Presidio is imported lazily and stays a real, non-default dependency. -
TamperEvidentPlugin: hash-chains every record (meta.hashover the record's own content plus the previous record'smeta.hash, stored asmeta.prev_hash), so editing, removing, or reordering a line in a written log breaks the chain from that point on. Ships with a staticTamperEvidentPlugin.verify_chain(records)to check a log after the fact. Opt-in — hashing every record has a real, measurable CPU cost. -
AlertingPluginbase class +SlackAlertPlugin,PagerDutyAlertPlugin, andEmailAlertPlugin: fires on ERROR/FATAL (or any configurablethreshold), with the actual send always running on a background thread so a slow or unreachable destination can never block the log call that triggered it. Repeated identical errors (same level + logger + message by default, or a customdedupe_key) withindedupe_window_secondscollapse into one follow-up alert carrying an occurrence count instead of spamming the destination once per record.send_alertfailures are caught and routed to the plugin's ownon_error, same as any other plugin hook. Tracking is bounded tomax_tracked_keysconcurrent dedupe windows — alerting degrades under extreme cardinality, logging itself never does. All three concrete plugins use only the stdlib (urllib,smtplib) — no new required dependency. -
Fixed a pre-existing gap surfaced by a new property-based test (see below):
Logger's per-transport dispatch had no error handling, so a transport that failed to format or write a given record (e.g.JSONFormatteron ametavalue containing a circular reference) would propagate the exception straight to the caller. Now caught and logged via the samelogging.getLogger("logquill")channelBatchingTransportalready uses, per transport, so one broken transport can't crash the caller or stop other attached transports from receiving the record. -
Added a
hypothesis-based property test (newdevdependency) that drives the plugin pipeline (ContextPlugin,RedactPlugin,PIIRedactPlugin,TamperEvidentPlugin) with adversarialmeta— deeply nested structures, unusual scalar types, non-JSON-serializable values, and circular references — asserting the pipeline never crashes the caller, only ever fails closed. -
New transports: SQL (
BaseSQLTransport+SQLiteTransport,PostgresTransport,MySQLTransport), NoSQL (MongoDBTransport,DynamoDBTransport,RedisTransport), message queues (BaseQueueTransport+KafkaTransport,RabbitMQTransport,SQSTransport,PubSubTransport), and cloud-native sinks (CloudWatchTransport,CloudLoggingTransport,AppInsightsTransport,DatadogTransport,ElasticsearchTransport,NewRelicTransport) — full parity withlogquill-js0.2.0. All of it sits on a new sharedBatchingTransportbase that bounds its buffer by both record count and estimated byte size, swaps the buffer out before sending so a synchronous re-entrant flush can't double-send, and catches a failing send rather than propagating it to the caller (logged via Python's stdliblogging.getLogger("logquill")) — a slow or down sink can't crash the process. Every optional backend driver (psycopg2-binary,pymysql,pymongo,boto3,redis,kafka-python,pika,google-cloud-pubsub,google-cloud-logging) is a lazy, injectable dependency behind a newpyproject.tomlextra (postgres,mysql,mongodb,redis,kafka,rabbitmq,pubsub,gcp-logging, and a sharedawsextra for CloudWatch/DynamoDB/SQS, all boto3-backed); a missing driver raises an actionableImportErrorrather than a cryptic one, and every test injects a hand-written fake instead of requiring a live service.SQLiteTransportneeds no extra at all (stdlibsqlite3).Two deliberate departures from
logquill-js's implementation, same outward behavior:AppInsightsTransportposts to Application Insights' public ingestion endpoint via stdliburllibinstead of an Azure SDK dependency, andSQSTransportdispatches its 10-message chunks sequentially rather than concurrently, since this project's dispatch is still fully synchronous end to end (true concurrency arrives once a non-blocking async worker exists).SyslogTransportisn't included here either, matchinglogquill-js0.2.0, which didn't ship it; it's a shared follow-up for both packages, not a Python-only gap.Also restructured
logquill/transport.py,console_transport.py,file_transport.py, andhttp_transport.pyinto a newlogquill/transports/subpackage (withsql/,nosql/,queue/, andcloud/subpackages) to hold the 17 new transports — a pure move, the publicfrom logquill import ...surface is unchanged. -
Plugin pipeline:
Pluginbase (before_log/after_log/on_error, all optional to override),ContextPlugin(merges fixed context intometa),RedactPlugin(replaces sensitivemetavalues by key, case- insensitive), andSamplingPlugin(probabilistically drops records).Loggernow acceptsplugins=[...]and gained.use(plugin)to register one and chain. A plugin hook that raises is caught, routed to that same plugin'son_error, and the pipeline continues — a broken plugin can't crash logging, verified by test. -
Added
.github/dependabot.yml: weekly version updates forpipdependencies and GitHub Actions. -
Added GitHub issue templates:
.github/ISSUE_TEMPLATE/bug_report.yml,feature_request.yml, and aconfig.ymlthat points security reports at private vulnerability reporting instead of a public issue. -
Added
.github/SECURITY.md: supported-versions policy and instructions to report vulnerabilities via GitHub's private vulnerability reporting instead of public issues. Linked from the README. -
Transports:
Transportbase (format/write/close),ConsoleTransport(colorized, ERROR/FATAL to stderr),FileTransport(size-based rotation), andHTTPTransport(batched, newline-delimited JSON over stdliburllib, with an injectablesenderfor tests or alternate backends).Loggernow acceptstransports=[...]and dispatches each record to them synchronously, and gained.close()to close all attached transports. Dispatch is still synchronous — a non-blocking queue/async path isn't implemented yet. Also addedCollectingTransport, an in-memory transport for tests. -
Added
CODE_OF_CONDUCT.md(Contributor Covenant v2.1),.github/CODEOWNERS,.github/PULL_REQUEST_TEMPLATE.md, andCONTRIBUTING.mddocumenting the PR workflow (branch naming, scoping, review/CI requirements, squash-merge). -
Core API:
Level(TRACE/DEBUG/INFO/WARN/ERROR/FATAL, matching logquill-js's numeric weights),parse_level(), theLogRecordshape,Loggerwith.trace()/.debug()/.info()/.warn()/.error()/.fatal()and.set_level(), and aFormatterprotocol with aJSONFormatterimplementation. Log calls return the record dict (orNonewhen filtered by level) — no transports or dispatch yet. -
Repo scaffold:
pyproject.toml, package skeleton, dev tooling (ruff, mypy --strict, pytest), pre-commit hooks, and CI workflow. -
Packaging metadata: expanded classifiers (OS, Topic) and keywords, added an
Issuesproject URL, and fixed theHomepage/Repository/ChangelogURLs to point at the actualnikhilvdev/logquill-pythonGitHub repo instead of a stale placeholder org. -
Added a pepy.tech download-count badge to the README for tracking installs.