Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,51 @@ jobs:
- run: pip install -e ".[dev]"

- run: pytest benchmarks

# Free-threaded (PEP 703, no-GIL) builds — the audit this phase requires
# for the worker/transport locking that backs `async_dispatch=False`
# (multiple application threads sharing one `Logger` and its transports).
# 3.13t and 3.14t get different dependency installs, not the same `dev`
# extras as the matrix above, because the free-threaded packaging
# ecosystem is still catching up as of this writing:
# - `orjson` (the `fast` extra) refuses to build for *any* free-threaded
# interpreter — its own build script rejects it outright, not just a
# missing-wheel gap. Skipped on both versions here.
# - `hypothesis`'s native extension, `jsonschema`'s `rpds-py`, and
# `twine`'s dependency chain are all PyO3-based and PyO3 itself
# refuses to target a free-threaded build below 3.14 — so on 3.13t
# only a pure-Python subset (`pytest`/`pytest-asyncio`/`pytest-cov`)
# installs at all. `tests/conftest.py` turns a missing `hypothesis`
# into a clean `collect_ignore_glob` for the property-based suites
# rather than a collection error. 3.14t has real `hypothesis` wheels
# and runs the full suite, `fast` aside.
# - `ruff`/`mypy` aren't re-run here — they don't depend on the
# interpreter's threading model, and the matrix job above already
# covers them on 3.13/3.14.
free-threaded:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13t", "3.14t"]

steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}

- run: python -c "import sys; assert not sys._is_gil_enabled(), 'GIL unexpectedly enabled'"

- name: Install dependencies
run: |
if [ "${{ matrix.python-version }}" = "3.13t" ]; then
pip install -e ".[http,apprise,otel]" "pytest>=8.0" "pytest-asyncio>=0.24" "pytest-cov>=5.0"
else
pip install -e ".[dev,http,apprise,otel]"
fi

- run: pytest

- run: pytest benchmarks
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ All notable changes to this project are documented in this file.

## Unreleased

- **Breaking: license change.** Starting with this release, `logquill` is
source-available under [FSL-1.1-MIT](LICENSE) (the Functional Source
License, MIT Future variant) instead of MIT. Free for internal use,
non-commercial education/research, and professional services you provide
to a licensee; a commercial product or service that competes with
logquill itself needs a separate commercial license from the author. Each
version converts to plain MIT two years after that version's own release.
**This is not retroactive** — 1.0.0 and every earlier release already on
PyPI remain MIT, unaffected. See the [License](README.md#license) section
of the README for the full explanation.
- Performance and platform hardening:
- `JSONFormatter(backend="orjson")` (`pip install logquill[fast]`) swaps
the stdlib `json` module for the Rust-backed `orjson` library — same
compact output, less CPU per call under sustained logging volume.
`orjson` is imported lazily and only if you ask for it; zero required
dependencies is unaffected. `orjson` doesn't currently support
free-threaded Python at all (its own build script refuses it, not a
missing-wheel gap), so `backend="json"` is what a free-threaded
interpreter gets regardless of what's installed.
- Thread-safety audit and fix: `BatchingTransport` (the shared base for
every SQL/NoSQL/queue/cloud transport), `HTTPTransport`, and
`FileTransport` now lock around their internal buffer/file-handle
mutation. Previously, a `Logger(async_dispatch=False)` (the default)
shared across application threads could race on these — two threads
passing the batch-size check before either flushed, or a rotation
closing a file handle out from under a concurrent write — silently
losing or duplicating records. Confirmed as a real, reproducible loss
(not just theoretical) under free-threaded Python before the fix, and
confirmed fixed after, via a 16-thread burst test. `AsyncWorker` was
already correctly locked (a `threading.Condition`-guarded queue) and
needed no change.
- `Logger.opt(lazy=..., depth=...)` now caches the returned `OptLogger`
per `(lazy, depth)` combination instead of allocating a fresh one on
every call — the common idiom is calling it inline on every log call
(`logger.opt(lazy=True).debug(...)`), so this removes a wrapper-object
allocation from that path. Bounded at 32 cached combinations per
`Logger` so a caller computing `depth` dynamically can't grow it
without limit. A new benchmark metric (`peak_bytes_filtered_opt_call`)
guards the filtered-and-deferred path specifically.
- The full test suite and the memory-budget benchmark suite now run in
CI on free-threaded Python (`3.13t` and `3.14t`, PEP 703) alongside the
existing GIL-enabled matrix. `hypothesis`'s native extension doesn't
yet ship (or build) wheels for free-threaded Python below 3.14 — an
upstream PyO3 constraint — so `tests/conftest.py` turns a missing
`hypothesis` into a clean skip of the property-based suites rather
than a collection error, and the `3.13t` CI job installs a smaller
dependency set accordingly.
- Internal reorganization: the top-level modules that had grown past a
single implementation each are now grouped into themed subpackages —
`core/` (`logger`, `levels`, `records`, `context`, `exceptions`,
Expand Down
105 changes: 97 additions & 8 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,13 +1,102 @@
MIT License
# Functional Source License, Version 1.1, MIT Future License

Copyright (c) 2026 Nikhil Arora
## Abbreviation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
FSL-1.1-MIT

## Notice

Copyright 2026 Nikhil Arora

## Terms and Conditions

### Licensor ("We")

The party offering the Software under these Terms and Conditions.

### The Software

The "Software" is each version of the software that we make available under
these Terms and Conditions, as indicated by our inclusion of these Terms and
Conditions with the Software.

### License Grant

Subject to your compliance with this License Grant and the Patents,
Redistribution and Trademark clauses below, we hereby grant you the right to
use, copy, modify, create derivative works, publicly perform, publicly display
and redistribute the Software for any Permitted Purpose identified below.

### Permitted Purpose

A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
means making the Software available to others in a commercial product or
service that:

1. substitutes for the Software;

2. substitutes for any other product or service we offer using the Software
that exists as of the date we make the Software available; or

3. offers the same or substantially similar functionality as the Software.

Permitted Purposes specifically include using the Software:

1. for your internal use and access;

2. for non-commercial education;

3. for non-commercial research; and

4. in connection with professional services that you provide to a licensee
using the Software in accordance with these Terms and Conditions.

### Patents

To the extent your use for a Permitted Purpose would necessarily infringe our
patents, the license grant above includes a license under our patents. If you
make a claim against any party that the Software infringes or contributes to
the infringement of any patent, then your patent license to the Software ends
immediately.

### Redistribution

The Terms and Conditions apply to all copies, modifications and derivatives of
the Software.

If you redistribute any copies, modifications or derivatives of the Software,
you must include a copy of or a link to these Terms and Conditions and not
remove any copyright notices provided in or with the Software.

### Disclaimer

THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.

IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.

### Trademarks

Except for displaying the License Details and identifying us as the origin of
the Software, you have no right under these Terms and Conditions to use our
trademarks, trade names, service marks or product names.

## Grant of Future License

We hereby irrevocably grant you an additional license to use the Software under
the MIT license that is effective on the second anniversary of the date we make
the Software available. On or after that date, you may use the Software under
the MIT license, in which case the following will apply:

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Expand Down
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Sibling to [`logquill` on npm](https://www.npmjs.com/package/logquill)
(`logquill-js`) — same log record shape, same level names, one mental model
across a Python + Node stack.

**License note:** starting with 2.0.0, new releases are source-available
under the [Functional Source License](#license) (FSL-1.1-MIT), not MIT.
Free for nearly everyone — internal use, non-commercial work, building a
product or service that isn't a logging framework — paid only if you're
building a competing logging/observability offering. Versions 1.0.0 and
earlier stay MIT forever; this doesn't change anything about those. See
[License](#license) below for what that actually means.

Status: pre-release, under active development. The core `Logger`, level
filtering, transports, the plugin pipeline, and agentic/harness tracing are
implemented; non-blocking async dispatch is not yet — see `CHANGELOG.md`
Expand Down Expand Up @@ -40,6 +48,7 @@ for what's landed so far.
- **Runtime intelligence** — `FlightRecorderPlugin` ships a run's buffered DEBUG trail only if it errors; `RunSummaryPlugin` emits one record with a run's total tokens/cost/tool calls/retries/errors; `AdaptiveSamplingPlugin` always keeps errors and slow spans while capping ordinary traffic to a bytes-per-second budget; `install_signal_level_handler`/`LevelFileWatcher`/`LevelEnvWatcher` change a logger's level without a restart — see [Runtime level control](#runtime-level-control)
- **Metrics bridge** — `MetricsPlugin` turns ERROR/FATAL records into a counter and any `meta.duration_ms` into a histogram, tagged by logger; ships with `OTelMetricsBackend` (default), `StatsDBackend`, and `PrometheusPushgatewayBackend` — see [Metrics bridge](#metrics-bridge)
- **Graph transports** — `Neo4jTransport`, `FalkorDBTransport`, `NeptuneTransport` write every record as a node and `parent_span_id` as a `PARENT_OF` edge, so a run's span tree is a Cypher traversal away with zero manual graph-writing code; one shared `BaseGraphTransport` means switching backends only means swapping the transport — see [Graph transports](#graph-transports)
- **A faster JSON backend, and free-threaded Python support** — `JSONFormatter(backend="orjson")` (`pip install logquill[fast]`) for less CPU per call under sustained volume; the worker and every batching transport lock around their shared buffers so a `Logger` used from multiple application threads is safe under both the GIL and free-threaded (PEP 703) builds — tested on 3.13t and 3.14t in CI — see [Thread safety](#thread-safety)

## Install

Expand Down Expand Up @@ -260,6 +269,28 @@ In a config file, name the formatter as a string in the transport's
`options`: `{"type": "console", "options": {"formatter": "text"}}` (`"json"`,
`"text"` or `"logfmt"`). To write your own, implement `format(record) -> str`.

#### A faster JSON backend

`JSONFormatter(backend="orjson")` (`pip install logquill[fast]`) swaps the
stdlib `json` module for the Rust-backed `orjson` library — same compact
output, meaningfully less CPU per call under sustained logging volume.
Zero required dependencies is unaffected: the default stays `backend="json"`,
and `orjson` is only imported if you ask for it, raising an actionable
`ImportError` right away if it isn't installed, not on the first `format()`
call:

```python
from logquill import ConsoleTransport, JSONFormatter, Logger

transport = ConsoleTransport(formatter=JSONFormatter(backend="orjson"))
logger = Logger("app.api", transports=[transport])
logger.info("user signed up", user_id=42)
```

`orjson` doesn't currently support free-threaded Python builds (3.13t/3.14t)
— that's an `orjson` limitation, not a LogQuill one, so `backend="json"`
(the default) is what those interpreters use regardless.

### SQL, NoSQL, message queue, and cloud-native transports

Every transport below shares one design: records are **always batched**
Expand Down Expand Up @@ -1463,6 +1494,43 @@ nothing catches the signal. A `SIGTERM` handler or `preStop` hook that calls
`logger.close(timeout=...)` closes that gap the same way `with_lambda` closes
it for a serverless freeze.

### Thread safety

A single `Logger` (and the transports/plugins it was built with) is safe to
call from multiple application threads at once, with `async_dispatch=True`
or without it:

- With `async_dispatch=False` (the default), every `.info()`/`.error()`/...
call dispatches to transports on the caller's own thread — so if several
threads share one `Logger`, they can genuinely run a transport's `write()`
concurrently. `BatchingTransport` (the shared base for the SQL/NoSQL/
queue/cloud transports), `HTTPTransport`, and `FileTransport` each lock
around their internal buffer/file-handle mutation for exactly this reason.
- With `async_dispatch=True`, every transport write and `after_log` hook
already runs serialized on the single `AsyncWorker` background thread
(itself built on a `threading.Condition`-guarded queue), so this is moot
for those two things specifically — but `before_log` plugin hooks still
run on the calling thread either way (they can filter/transform the
record, so a later hook needs to see the result in order), which matters
for the next point regardless of `async_dispatch`.
- A plugin with its own internal counter — `RunPlugin`'s step counter,
`SamplingPlugin`/`AdaptiveSamplingPlugin`'s rate state,
`TamperEvidentPlugin`'s hash chain — mutates that state in `before_log`,
always on the calling thread. Sharing one plugin instance (via one
`Logger`, or via `.child()`) across multiple threads that log
concurrently isn't guarded by a lock today; `TamperEvidentPlugin`
specifically depends on its hash chain being appended to in the same
order records are meant to chain in, so a multi-threaded audit logger is
the case to be most careful with. Give each thread (or each request) its
own `.child()` logger if you need per-thread isolation, which is also the
simpler mental model regardless of this caveat.

Verified under both the GIL and free-threaded (PEP 703, `python3.13t`/
`python3.14t`) builds in CI: the full test suite and the memory-budget
benchmarks (see [Development](#development)) run on both, plus a dedicated
burst test hammering one `BatchingTransport` from 16 threads at once and
checking every record arrived exactly once.

## Context propagation, exception capture & the stdlib bridge

`bind_context()` binds request-scoped values for a `with` block — every
Expand Down Expand Up @@ -1851,8 +1919,53 @@ pre-commit install
ruff check .
mypy logquill
pytest
pytest benchmarks # memory budgets for the logging hot path
```

CI also runs the full suite and the benchmarks on free-threaded Python
(`3.13t`/`3.14t`) — see [Thread safety](#thread-safety). `hypothesis` and
`orjson` (the `fast` extra) don't currently build for `3.13t`, an upstream
PyO3/orjson limitation, not a LogQuill one, so that job installs a smaller
dependency set there; `tests/conftest.py` turns a missing `hypothesis` into
a clean skip of the property-based suites rather than a collection error.

See [CONTRIBUTING.md](CONTRIBUTING.md) for the PR workflow, the
[Code of Conduct](CODE_OF_CONDUCT.md) for community standards, and
[SECURITY.md](.github/SECURITY.md) for how to report a vulnerability.

## License

**Versions 1.0.0 and earlier are MIT**, full stop — already published, and
relicensing later code doesn't and can't change the terms of what's already
out there. If you depend on one of those versions, nothing here affects you.

**Version 2.0.0 and later are [FSL-1.1-MIT](LICENSE)** (the [Functional
Source License](https://fsl.software), MIT Future variant). In plain terms:

- You can use, modify, self-host, and redistribute logquill for **almost
anything** — your own app or service, internal tooling, a client project,
non-commercial education or research, or as part of professional services
you provide to someone else — with no payment or extra permission needed.
- The one thing you can't do without a separate commercial license is use
logquill to build a **competing product or service**: one that substitutes
for logquill itself, substitutes for another product/service built on
logquill, or offers substantially the same functionality. That's aimed at
"take this, repackage it, sell it as your own logging/observability
product" — not at ordinary use of a logging library inside whatever you're
actually building.
- **Each version converts to plain MIT two years after that version's own
release date.** So 2.0.0 becomes MIT two years after 2.0.0 ships,
independent of whatever the latest version is by then — the restriction
only ever applies to recent releases, never indefinitely.
- Redistributing logquill (modified or not) requires keeping the copyright
notice and a copy of (or link to) these terms — that's the "credit"
obligation the license itself carries.

Need a commercial license (or just have questions about whether your use
case needs one)? Open an issue, or reach out to the author directly.

This is a source-available license, not an OSI-approved open-source one —
drafted from the official FSL template and not customized beyond filling in
the licensor/year, but also not reviewed by a lawyer on either side of a
specific deal. If a commercial license matters to your business, have your
own counsel look at the exact terms before you rely on them.
Loading
Loading