Changelog
All notable changes to LogCore are documented here.
Format follows Keep a Changelog. Versioning follows Semantic Versioning.
Unreleased
0.1.7 - 2026-08-15
Performance, correctness and ecosystem release. Two security fixes, a ~2.6x speedup on the text path, and the integration points needed to make LogCore the logger for a whole process rather than only for the lines you write yourself.
Security
Nested secrets are no longer logged in cleartext. Redaction only ever matched top-level keys, because
_log()stringified dicts and lists before any formatter saw them — sologger.info("login", user={"password": "..."})emitted the password verbatim in JSON mode despitepasswordbeing a default redact field. Redaction is now structural and recurses through dicts, lists and tuples at any depth (bounded by a depth and node cap).Message bodies are now redacted in JSON mode.
logger.info("password=hunter2")was masked byTextFormatterbut passed throughJSONFormatteruntouched, so switching tojson=Truefor production silently weakened redaction.
Added
configure_stdlib()routes the stdlib root logger through LogCore’s formatters, so third-party output (uvicorn, sqlalchemy, requests, celery) is formatted consistently with your own. Includes aquiet=shortcut for turning down noisy libraries, plusreset_stdlib().dict_config_formatter()for applications that configure logging throughlogging.config.dictConfig.CorrelationIdMiddleware(ASGI) andWSGICorrelationIdMiddleware(WSGI): zero-dependency correlation-ID propagation for FastAPI, Starlette, Flask and Django. AdoptsX-Request-IDor the W3Ctraceparenttrace-id, echoes it on the response, and closes out the correlation scope — which also releases tail-sampling buffers.Opt-in non-blocking logging via
async_logging=True(LOGCORE_ASYNC), moving handler I/O onto a background thread with a bounded, load-shedding queue. Addslogcore.flush(),logcore.shutdown(),logcore.dropped_record_count()andLogCoreLogger.flush().exception_typeandexception_messagefields alongside the existingexceptionstring, for grouping in Elasticsearch/Datadog/Sentry.console=Falseto disable console output, andconsole_stream="stdout"for containers that want logs on stdout.stacklevel=on log calls, for user-written logging wrappers.Sampler(max_active_buffers=...)and aevicted_buffersstat.Much larger public API:
JSONFormatter,TextFormatter,LogCoreLogger,LogCoreConfig,Timer,AsyncTimer,correlation_id_context,generate_correlation_id,DecisionandSamplerStatsare now exported from the package root. Previously users had to reach into private modules to annotate the typeget_loggerreturns.New env vars:
LOGCORE_PROPAGATE,LOGCORE_CONSOLE,LOGCORE_CONSOLE_STREAM,LOGCORE_ASYNC,LOGCORE_QUEUE_SIZE.Python 3.13 in the CI matrix and classifiers; macOS and Windows CI jobs.
Changed
Dict and list extras now serialize as real JSON instead of Python-repr strings.
{"user": "{'name': 'bob'}"}becomes{"user": {"name": "bob"}}. The old form could not be indexed by any log pipeline. Anything parsing those repr strings needs updating.Records no longer propagate to the root logger. Previously every LogCore record was printed twice as soon as anything in the process called
logging.basicConfig(). Passpropagate=Trueto restore the old behavior.Extras colliding with
LogRecordattributes are emitted askey_instead of being silently dropped.name,module,filename,process,threadand ~18 other reserved words previously vanished with no error. Warns once per key.Invalid
LOGCORE_*environment values now emit aUserWarningand fall back to the default, instead of being silently ignored. A typo’dLOGCORE_SAMPLE_RATEused to mean shipping 100% of logs with nothing to point at.Redacted values in text output render as
password=my***rather thanpassword="my***", matching how every other extra field is rendered.Timer/AsyncTimerfailures now capture a full traceback viaexc_infoinstead of a bareexception=str(exc)field, and attribute the record to user code rather than tologcore/utils.py.colorama.init()is no longer called at import time; it runs on first use of a coloured formatter. Colour selection honorsNO_COLOR,FORCE_COLORandTERM=dumb.Coverage now enforces an 85% floor with branch coverage enabled.
Fixed
~2.6x faster text logging (25.8 → 9.9 µs/call) and ~18% faster JSON (12.0 → 9.8 µs/call), measured by
examples/benchmark.pyon Python 3.13. Attribution, measured by reintroducing each change individually rather than inferred from a profiler:TextFormatterran a 13-branchIGNORECASEregex substitution over every rendered line. Removing it accounts for ~13.9 µs of the ~15.9 µs saved on the text path, and is why text output used to be slower than JSON._find_callercalledos.path.abspathper stack frame — agetcwdsyscall each time — to compute a value neither formatter ever emitted. Worth ~1 µs/call on both paths: real, but a small share of the total.The remainder (~1 µs) is timestamp caching, one less dict allocation per record, compact JSON separators and a precomputed level lookup.
Unbounded memory growth in tail-based sampling.
Samplertracked correlation IDs in dicts that were never evicted, so any service usingset_correlation_id()(rather than thewith_correlation_id()context manager) leaked one buffer per request indefinitely. Now LRU-capped atmax_active_buffers(default 1000).Sampling decisions are made before the record is built, so a dropped record no longer pays for frame inspection,
makeRecord, the OpenTelemetry span lookup or field coercion. Sampler lock acquisitions per record went from 2–3 to 1.File handlers no longer receive colour settings derived from
sys.stderr.isatty(), which wrote raw ANSI escape codes into log files when running from a terminal with colorama installed.Reconfiguring a logger now closes its old handlers instead of dropping the references, which leaked a file descriptor each time.
Exception tracebacks are formatted once and cached on
record.exc_textinstead of being re-walked by every attached handler.logger.exception(msg, exc_info=...)no longer ignores an explicitly passedexc_info, and passing an exception instance now works.record.stack_infois rendered by both formatters instead of being dropped.Payload shapes that
json.dumpsrejects — self-referential structures and non-primitive dict keys — no longer cause the record to be dropped. Because stdlib logging swallows formatter exceptions, these would have vanished silently; they now serialize with[CIRCULAR]and stringified keys.Logging a
namedtupleno longer raises inside the formatter.__version__is read from installed package metadata. It was hardcoded to"0.1.5"whilepyproject.tomlsaid0.1.6, so the published 0.1.6 wheel reported the wrong version and the docs site rendered the wrong release.The release workflow no longer publishes to PyPI without running tests, lint or type checks first, and it verifies the tag matches the package version. The duplicate publish job in
ci.yml— which raced the release workflow for the same version and guaranteed one red run per release — has been removed.The
security-scanCI job actually reports now; both tools ran with|| trueand the report was never surfaced, so it could not fail.Docs are built on pull requests, not only after merge to
main.Deleted
tests/pytest.ini, which shadowed the pyproject pytest config and silently disabled coverage and asyncio settings forpytest tests/.Added
MANIFEST.in. Without it the sdist fell back to a distutils-era default that shippedtests/test*.pybut notconftest.pyortests/__init__.py, so the source distribution’s test suite could not be collected. Downstream packagers build from the sdist and run the tests.
Notes
The PyPI publish step still uses a long-lived
PYPI_API_TOKEN. Migrating to Trusted Publishing (OIDC) is recommended and requires a one-time publisher entry on PyPI first, so it was left for a follow-up.
0.1.6 - 2026-05-27
Added
Log sampling. New
Samplerclass combining rate-based, level-aware, and tail-based strategies. Tail-based mode buffers records under the current correlation_id and flushes them on the firstalways_keeprecord (default WARNING/ERROR/CRITICAL), so failed requests get full history while successful ones cost nothing. Buffer is bounded per correlation_id to prevent unbounded memory growth.Samplerexported from top-levellogcorepackage.get_logger(..., sampler=...)andget_logger(..., sample_rate=...)shortcuts.LogCoreLogger.flush_sample_buffer(cid)for users who set correlation_id directly without the context manager.Environment variables:
LOGCORE_SAMPLE_RATE,LOGCORE_SAMPLE_TAIL,LOGCORE_SAMPLE_BUFFER_SIZE,LOGCORE_SAMPLE_ALWAYS_KEEP.GitHub issue templates (
bug_report.md,feature_request.md) and a pull request template under.github/..flake8config so localflake8 logcore testsmatches CI (max-line-length=88).
Changed
Minimum supported Python version raised from 3.8 to 3.9. CI stopped running the 3.8 matrix entry; the classifier and
requires-pythonnow reflect that.LogCoreLogger.with_correlation_id()now discards any tail-buffered records on clean exit.Removed the
Documentationproject URL that pointed back to the README; it will return once a dedicated docs site is published.
0.1.5 - 2026-05-20
Fixed
TextFormatternow applies the same partial-masking logic (se***) asJSONFormatterfor redacted fields. Previously it emitted a flat[REDACTED]string, contradicting documented behavior.TextFormatterno longer emits stdlibLogRecordinternals (exc_info,exc_text,stack_info,taskName) as extra key=value pairs on every line.LogRecord.filename,lineno, andfuncNameare now populated with the real call site instead of the hardcoded placeholder"(unknown file)"/0.exc_info != Trueguard in both formatters replaced with a plain truthiness check — the previous form was a no-op (a tuple is never== True) and could mask edge cases.
Changed
skip_fieldsde-duplicated into a single module-level_STDLIB_LOG_FIELDSfrozenset shared by bothJSONFormatterandTextFormatter. Adding a field to suppress now only requires one change.get_loggeremits aUserWarningwhen called with configuration arguments for a name that already has a cached logger. Previously the replacement was silent, making it easy to create dangling references.
0.1.4 - 2025-01-15
Added
OpenTelemetry integration:
trace_idandspan_idare automatically injected into log records when an active span exists. Zero configuration required; installlogcore[otel]to enable.AsyncTimercontext manager forasync with logger.time(...)in asyncio applications.is_async_context()utility to auto-detect running event loop.Partial masking for redacted fields: values longer than 4 characters show a short prefix (e.g.
se***) instead of a blanket[REDACTED], making it possible to correlate log lines without leaking secrets.logcore/py.typedmarker for PEP 561 compliance.examples/benchmark.pywith measured throughput numbers.
Changed
JSON formatter timestamps now emit UTC with timezone offset (
+00:00) for unambiguous parsing by log aggregators.is_async_context()now usesasyncio.get_running_loop()(raisesRuntimeErrorwhen no loop is running) instead ofasyncio.current_task()(returnedNonein non-async contexts, making detection unreliable).Optional[Set[str]]type annotation used consistently acrossLogCoreConfig,JSONFormatter, andTextFormatter— removes false mypy errors under strict mode.Removed redundant
LogLevel.WARNenum member;LogLevel.from_string("WARN")continues to normalise toLogLevel.WARNING.
Fixed
Logger caching lock now uses
threading.RLockto prevent deadlocks whenget_loggeris called recursively from within a handler.
0.1.3 - 2024-12-01
Changed
Complete migration from
logforgetologcorepackage name.Updated all internal references, classifiers, and PyPI metadata.
0.1.2 - 2024-11-15
Added
Environment variable configuration via
LOGCORE_*prefix.LOGCORE_REDACT_FIELDSenv var accepts a comma-separated list.File logging with
RotatingFileHandler; configurablemax_file_sizeandbackup_count.with_correlation_id()context manager usingcontextvarsfor per-task isolation in async code.
Changed
get_loggerreturns a cached instance per name; passing configuration parameters forces a new instance.
0.1.1 - 2024-10-20
Added
logger.time()context manager that logs start, completion, andduration_ms.logger.exception()convenience method that captures the current traceback.Colorama-based coloured output in
TextFormatter; falls back gracefully when colorama is absent.
0.1.0 - 2024-10-01
Added
Initial release as
logforge.get_logger(name)single-entrypoint API.JSONFormatterandTextFormatter.RedactingFormatterbase class with configurable sensitive-field redaction.Thread-safe logger registry.
MIT license.