A minimal, ergonomic python logging wrapper
Project description
ergolog
A minimal, ergonomic Python logging wrapper
from ergolog import eg — one entry point for tags, counters, timers, wide events, and trace. Everything context-scoped, thread-safe, and composable.
- Named loggers —
eg('name'), nested withone('two') - Tags — stack and nest for context; keyword tags, callable values, auto-UUIDs
- Counters — live-updating tag values; enumerate loops, accumulate totals
- Timers — elapsed timing with
.lap()split times and named laps - Wide events — accumulate context, emit a single line; counters, timers, and laps resolve at emit time
- Trace — function-level decorator for timing and entry logging
- Thread-safe — tag isolation via
contextvars
Why ergolog?
ergolog is built around two ideas: tags carry ambient context, and wide events capture operation outcomes.
Tags propagate through the call stack. When you open a with eg.tag(request_id='abc') block, every log line inside it carries [request_id=abc]. Tags nest, so a child scope adds to the stack instead of replacing it. The stack unwinds automatically on exception, and because tags are stored in contextvars, each thread and async task maintains its own isolated context.
Wide events complement tags. Where tags answer "what context am I in?", events answer "what happened?". An event accumulates fields like counters, timers, named laps, and custom data, then emits a single structured line at operation boundaries. Timer and counter values evaluate at emit time, so the final line always shows total duration and all intermediate splits without manual bookkeeping.
ergolog won't stomp on your app's logging setup — set ERGOLOG_NO_AUTO_SETUP=1 and it does nothing on import.
Installation
uv add ergolog
# or
pip install ergolog
Configuration
No configuration needed — ergolog auto-configures on import with colored output to stdout.
from ergolog import eg
eg.info('works immediately')
To customize at runtime, use eg.config:
eg.config.add_output('file', path='app.jsonl', format='json') # JSON to file (append mode)
eg.config.set_format('json') # Switch stdout to JSON
eg.config.remove_output('stdout') # Remove an output
Valid formats: 'default' (colored), 'plain' (no ANSI), 'json' (JSONL). Valid outputs: 'stdout', 'stderr', 'file'.
For log level and propagation, use the standard logging API:
import logging
eg._logger.setLevel(logging.WARNING) # filter by level
eg._logger.propagate = False # prevent double-logging in frameworks
Using ergolog in a library? Set ERGOLOG_NO_AUTO_SETUP=1 before importing so the host app owns logging:
import os
os.environ['ERGOLOG_NO_AUTO_SETUP'] = '1'
from ergolog import eg
Environment variables (all "off switches"):
ERGOLOG_NO_AUTO_SETUP— don't configure any handlers on importERGOLOG_NO_COLORS— disable ANSI color outputERGOLOG_NO_TIME— disable timestamp prefix
Basic Usage
from ergolog import eg
eg.debug('debug')
eg.info('info')
eg.warning('warning')
eg.error('error')
eg.critical('critical')
2025-04-25 15:30:01,234 [DEBUG ] ergo (main.py:3) debug
2025-04-25 15:30:01,235 [INFO ] ergo (main.py:4) info
2025-04-25 15:30:01,236 [WARNING ] ergo (main.py:5) warning
2025-04-25 15:30:01,237 [ERROR ] ergo (main.py:6) error
2025-04-25 15:30:01,238 [CRITICAL] ergo (main.py:7) critical
Colors: DEBUG is blue, INFO is green, WARNING is yellow, ERROR is red, CRITICAL is magenta. Timestamps and file locations are dimmed. Set
ERGOLOG_NO_COLORS=1to disable.
Named Loggers
from ergolog import eg
eg('test').debug('named logger')
15:30:01,234 [DEBUG ] ergo.test (main.py:3) named logger
Child loggers:
one = eg('one')
two = one('two')
two.info('child logger')
15:30:01,235 [INFO ] ergo.one.two (main.py:4) child logger
Tags
from ergolog import eg
with eg.tag('tag1'):
eg.info('one tag')
with eg.tag('tag2'):
eg.info('two tags')
eg.info('one tag again')
15:30:01,235 [INFO ] ergo [tag1] (main.py:4) one tag
15:30:01,236 [INFO ] ergo [tag1, tag2] (main.py:6) two tags
15:30:01,237 [INFO ] ergo [tag1] (main.py:7) one tag again
Tag Decorator
from ergolog import eg
@eg.tag('inner')
def inner():
eg.info('test')
@eg.tag('outer')
def outer():
eg.debug('before')
inner()
eg.debug('after')
eg.debug('start')
outer()
eg.debug('end')
15:30:01,234 [DEBUG ] ergo (main.py:14) start
15:30:01,235 [DEBUG ] ergo [outer] (main.py:9) before
15:30:01,236 [INFO ] ergo [outer, inner] (main.py:5) test
15:30:01,237 [DEBUG ] ergo [outer] (main.py:12) after
Keyword Tags
from ergolog import eg
with eg.tag(keyword='tags', comma='multiple'):
eg.debug('')
with eg.tag('regular tag'):
eg.info('')
with eg.tag(more='keywords'):
eg.info('')
eg.debug('')
15:30:01,234 [DEBUG ] ergo [keyword=tags, comma=multiple] (main.py:4)
15:30:01,235 [INFO ] ergo [keyword=tags, comma=multiple, regular tag] (main.py:6)
15:30:01,236 [INFO ] ergo [keyword=tags, comma=multiple, regular tag, more=keywords] (main.py:8)
15:30:01,237 [DEBUG ] ergo [keyword=tags, comma=multiple] (main.py:9)
Auto-generated IDs
from ergolog import eg
with eg.tag(job=eg.uid):
eg.info('first')
with eg.tag(job=eg.uid):
eg.info('nested')
eg.info('first again')
15:30:01,235 [INFO ] ergo [job=34bfbe] (main.py:4) first
15:30:01,236 [INFO ] ergo [job=34bfbe, job=80dbc9] (main.py:6) nested
15:30:01,237 [INFO ] ergo [job=34bfbe] (main.py:7) first again
Any zero-arg callable works as a tag value — it's evaluated once when the tag context is entered.
Counters
from ergolog import eg
counter = eg.counter()
with eg.tag(step=counter):
eg.info('start') # [step=0]
counter += 1
eg.info('middle') # [step=1]
counter += 1
eg.info('end') # [step=2]
15:30:01,235 [INFO ] ergo [step=0] (main.py:4) start
15:30:01,236 [INFO ] ergo [step=1] (main.py:6) middle
15:30:01,237 [INFO ] ergo [step=2] (main.py:8) end
Counters are tag values that update live — each log line shows the current value.
Loop enumeration
loops = eg.counter()
with eg.tag(i=loops):
for item in loops.count(['a', 'b', 'c']):
eg.info(f'item {item}')
15:30:01,235 [INFO ] ergo [i=1] (main.py:4) item a
15:30:01,236 [INFO ] ergo [i=2] (main.py:4) item b
15:30:01,237 [INFO ] ergo [i=3] (main.py:4) item c
Accumulation
total = eg.counter()
with eg.tag(bytes=total):
total += 1024
eg.info('chunk') # [bytes=1024]
total += 512
eg.info('chunk') # [bytes=1536]
15:30:01,235 [INFO ] ergo [bytes=1024] (main.py:4) chunk
15:30:01,236 [INFO ] ergo [bytes=1536] (main.py:6) chunk
Timers
from ergolog import eg
with eg.timer(lambda t: eg.debug(f'took {t}S')):
eg.info('before')
# ... do stuff
eg.info('after')
15:30:01,235 [INFO ] ergo (main.py:4) before
15:30:01,236 [INFO ] ergo (main.py:6) after
15:30:01,237 [DEBUG ] ergo (main.py:3) took 0.101S
Or use as a context manager to capture elapsed time:
with eg.timer() as t:
# ... do stuff
pass
eg.debug(f'took {t} S')
15:30:01,235 [DEBUG ] ergo (main.py:5) took 0.123 S
Laps
Use .lap() to take split times without stopping the timer. Returns elapsed seconds as a float:
with eg.timer() as t:
fetch_data()
fetch_time = t.lap() # returns float, timer keeps running
process_data()
process_time = t.lap() # returns float from start
eg.debug(f'fetch={fetch_time:.3f}s process={process_time:.3f}s total={t.elapsed:.3f}s')
15:30:01,235 [DEBUG ] ergo (main.py:6) fetch=0.103s process=0.456s total=0.456s
Use .lap('name') to record named laps — these are auto-collected by events:
with eg.timer() as t:
fetch_data()
t.lap('fetch') # records lap name + time
process_data()
t.lap('process') # records lap name + time
# t.laps == {'fetch': 0.103, 'process': 0.456}
15:30:01,235 [DEBUG ] ergo (main.py:3) fetch=0.103s process=0.456s total=0.456s
Timers as Tag Values
Timers can be used as keyword tag values, showing live elapsed time on each log line:
t = eg.timer()
with eg.tag(elapsed=t):
eg.info('start')
sleep(0.1)
eg.info('middle')
sleep(0.1)
eg.info('end')
15:30:01,234 [INFO ] ergo [elapsed=0.000s] (main.py:3) start
15:30:01,334 [INFO ] ergo [elapsed=0.100s] (main.py:5) middle
15:30:01,434 [INFO ] ergo [elapsed=0.200s] (main.py:7) end
Trace
eg.trace() is a debugging tool for local development. It logs function entry, elapsed time, and optionally arguments and return values. A WARNING is emitted at decoration time as a reminder not to leave it in production code.
from ergolog import eg
@eg.trace()
def my_function(a, b):
return a + b
my_function(2, 2)
15:30:01,234 [WARNING ] ergo [trace=my_function] (ergolog.py:241) registering trace
15:30:01,235 [DEBUG ] ergo [trace=my_function] (ergolog.py:252) done in 0.000S
By default, arguments and return values are omitted. Use log_args=True when you need full visibility into a function call.
Structured Logging
Tags are available on LogRecord as both record.tags (display string) and record.tag_list (raw list), so custom formatters can access them for JSON or structured output.
Wide Events
Wide events accumulate context throughout an operation and emit a single log line at the end. Use them to capture operation boundaries — "what happened" rather than "how we got here."
from ergolog import eg
# Context manager (auto-emit on exit)
with eg.event(user='alice', action='checkout') as e:
e.set(cart={'items': 3, 'total': 9999})
e.set(payment={'method': 'card'})
# On exit: emits one log line with all context + duration
15:30:01,235 [INFO ] ergo (main.py:4) user=alice action=checkout cart={'items': 3, 'total': 9999} payment={'method': 'card'} | duration=0.234s
Manual Emit
e = eg.event(user='bob')
e.set(action='search', query='ergonomics')
e.emit()
15:30:01,235 [INFO ] ergo (main.py:3) user=bob action=search query=ergonomics | duration=0.001s
Outcome Levels
Events default to INFO. Use e.error() or e.warn() to mark the outcome:
# Success → INFO
with eg.event(user='alice') as e:
process_payment()
# Success with concern → WARNING
with eg.event(user='bob') as e:
result = process_payment()
if result.used_fallback:
e.warn('used fallback payment method')
# Failure → ERROR
try:
with eg.event(user='charlie') as e:
raise ValueError('insufficient funds')
except ValueError:
pass
15:30:01,235 [INFO ] ergo (main.py:2) user=alice | duration=0.001s
15:30:01,235 [WARNING ] ergo (main.py:7) warning=used fallback payment method user=bob | duration=0.001s
15:30:01,235 [ERROR ] ergo (main.py:12) ValueError: insufficient funds user=charlie | duration=0.002s
Sealed After Emit
Events emit exactly once. After emit(), further set() calls are ignored:
e = eg.event(user='alice')
e.emit()
e.set(ignored='data') # Ignored, event is sealed
15:30:01,235 [INFO ] ergo (main.py:2) user=alice | duration=0.001s
Capturing Tags
Events capture the current tag stack at emit time:
with eg.tag(request_id='abc123'):
e = eg.event(operation='tagged')
e.set(extra='data')
e.emit()
15:30:01,235 [INFO ] ergo (main.py:2) operation=tagged extra=data request_id=abc123 | duration=0.001s
Counters in Events
Counters passed to e.set() evaluate at emit time — the event shows their final value:
counter = eg.counter()
with eg.event(op='batch') as e:
e.set(processed=counter)
for item in items:
process(item)
counter += 1
# Event shows: processed=5 (or whatever the final count is)
15:30:01,235 [INFO ] ergo (main.py:7) op=batch processed=5 | duration=0.034s
Timers in Events
Timers passed to e.set() evaluate at emit time — the event shows total elapsed:
t = eg.timer()
with eg.event(op='export') as e:
e.set(duration=t)
export_data()
# Event shows: duration=1.234 (total elapsed)
15:30:01,235 [INFO ] ergo (main.py:4) op=export duration=1.234 | duration=1.234s
Named Laps in Events
Named laps on timers are auto-collected into events. This is the primary mechanism for breaking down multi-stage operations:
t = eg.timer()
with eg.event(op='pipeline') as e:
e.set(duration=t)
fetch_data()
t.lap('fetch')
process_data()
t.lap('process')
save_results()
t.lap('save')
# Event includes: duration=0.789 fetch=0.101 process=0.456 save=0.232
15:30:01,235 [INFO ] ergo (main.py:9) op=pipeline duration=0.789 fetch=0.101 process=0.456 save=0.232 | duration=0.789s
You can also set lap values explicitly for full control:
with eg.event(op='task') as e:
t = eg.timer()
fetch_data()
e.set(fetch_time=t.lap())
process_data()
e.set(process_time=t.lap())
15:30:01,235 [INFO ] ergo (main.py:6) op=task fetch_time=0.101 process_time=0.456 | duration=0.456s
When to Use Events vs Regular Logs
| Pattern | Purpose |
|---|---|
Regular logs (eg.info/debug/warning) |
Trace execution, debug flow |
Wide events (eg.event()) |
Capture operation outcome |
Use both together for complete visibility:
with eg.event(operation='export') as e:
e.set(format='pdf', pages=24)
eg.debug('starting export')
export_to_pdf(doc)
eg.debug('export complete')
# Event emits: what happened (one line)
15:30:01,234 [DEBUG ] ergo [operation=export] (main.py:3) starting export
15:30:01,345 [DEBUG ] ergo [operation=export] (main.py:5) export complete
15:30:01,345 [INFO ] ergo (main.py:6) operation=export format=pdf pages=24 | duration=0.111s
JSON Formatter
For structured logging to files or log aggregation systems:
from ergolog import eg
# Switch to JSON output
eg.config.set_format('json')
eg.info('hello')
Output (one line per log):
{"timestamp":"2024-01-15T10:23:45.123Z","level":"INFO","name":"ergo","message":"hello","tags":{},"location":{"file":"main.py","line":4,"function":"<module>"}}
For wide events, the full context is included:
{"timestamp":"...","level":"INFO","name":"ergo","message":"user=alice action=checkout ...","event":{"user":"alice","action":"checkout","cart":{"items":3},"duration_s":0.234},"tags":{"request_id":"abc123"}}
You can also send JSON to a file while keeping colored output on stdout:
eg.config.add_output('file', path='app.jsonl', format='json')
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ergolog-0.2.0.tar.gz.
File metadata
- Download URL: ergolog-0.2.0.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbb821b21c8f4d8fffdd5f22e3b2a5e0bdbc91e6388707d075ab793b15722cda
|
|
| MD5 |
2ae717b8e77238c6f8d93f114f4d5be1
|
|
| BLAKE2b-256 |
a4619db7eaf016eebd20d727b82272e6cc74655ff32cf2aaa7f8c9ff393b0df4
|
File details
Details for the file ergolog-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ergolog-0.2.0-py3-none-any.whl
- Upload date:
- Size: 15.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80ac82d29408b3c8e218b711dab5071fe3c36f2af9f1e8fd5320a048294bd6b2
|
|
| MD5 |
df63330b66b135743db824fb61021fcd
|
|
| BLAKE2b-256 |
cc3365d1a54f9a4c31bb46b05d501bdd4e902aa99e091e1d7c221656f2506e80
|