log-foundry 0.2.1.dev3__tar.gz → 0.2.1.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/PKG-INFO +64 -5
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/README.md +63 -4
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/pyproject.toml +1 -1
- log_foundry-0.2.1.dev4/src/log_foundry/__init__.py +73 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/decorator.py +19 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/worker.py +79 -0
- log_foundry-0.2.1.dev3/src/log_foundry/__init__.py +0 -45
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/LICENSE +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/api.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/config.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/console.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/context.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/model.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.2.1.dev3 → log_foundry-0.2.1.dev4}/src/log_foundry/sinks/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: log-foundry
|
|
3
|
-
Version: 0.2.1.
|
|
3
|
+
Version: 0.2.1.dev4
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -206,6 +206,12 @@ never crashes the worker or the app. If the bounded queue fills completely, new
|
|
|
206
206
|
dropped (newest-first) and counted rather than blocking your code. On `shutdown()` the worker
|
|
207
207
|
stops, sweeps anything still queued into one final batch, emits it, and closes the sink.
|
|
208
208
|
|
|
209
|
+
Both triggers can be pre-empted: `flush()` puts a marker in the queue and the worker emits the
|
|
210
|
+
pending pile the moment it reaches it, ignoring the count and time triggers. Because the queue
|
|
211
|
+
is FIFO, everything submitted before the call is necessarily ahead of that marker — which is
|
|
212
|
+
exactly why the guarantee is "events submitted before this call", and why concurrent
|
|
213
|
+
submissions from other threads may or may not be included.
|
|
214
|
+
|
|
209
215
|
## Usage
|
|
210
216
|
|
|
211
217
|
### `configure(...)`
|
|
@@ -494,17 +500,70 @@ a failing sink with backoff, and applies backpressure so a slow or down sink can
|
|
|
494
500
|
back-pressure the app: when its bounded queue is full it drops the newest submissions and counts
|
|
495
501
|
them (`worker.dropped`) rather than stalling.
|
|
496
502
|
|
|
497
|
-
Because delivery is asynchronous, drain before the process exits
|
|
503
|
+
Because delivery is asynchronous, drain before the process exits. There are two drains, and
|
|
504
|
+
which one you want depends on whether the process is about to end:
|
|
498
505
|
|
|
499
506
|
```python
|
|
500
507
|
import log_foundry as lf
|
|
501
508
|
|
|
502
|
-
lf.
|
|
509
|
+
lf.flush() # drain to the sink and keep going; returns True when everything landed
|
|
510
|
+
lf.shutdown() # drain, close the sink, and stop for good; blocks until drained
|
|
503
511
|
```
|
|
504
512
|
|
|
513
|
+
| | `flush()` | `shutdown()` |
|
|
514
|
+
|---|---|---|
|
|
515
|
+
| Drains buffered events | yes | yes |
|
|
516
|
+
| Closes the sink | no | yes |
|
|
517
|
+
| Worker survives | yes | **no** — it never comes back |
|
|
518
|
+
| Repeatable | yes | idempotent, but only the first call does anything |
|
|
519
|
+
| Use it | before returning from a handler, or at a checkpoint | once, as the process exits |
|
|
520
|
+
|
|
505
521
|
`shutdown()` is also registered via `atexit`, so a normal exit flushes automatically — call it
|
|
506
|
-
explicitly when you need to be certain the tail reached the sink before a fast exit
|
|
507
|
-
end of a short script
|
|
522
|
+
explicitly when you need to be certain the tail reached the sink before a fast exit, e.g. at the
|
|
523
|
+
end of a short script. It is idempotent.
|
|
524
|
+
|
|
525
|
+
`flush(timeout=5.0)` returns `True` when every event submitted before the call has been passed
|
|
526
|
+
to the sink, and `False` if that did not happen within `timeout` (or the worker was already shut
|
|
527
|
+
down). It never raises — a logging call must not be the reason your function fails. Passing
|
|
528
|
+
`timeout=None` waits indefinitely, which is unsafe anywhere with an execution deadline.
|
|
529
|
+
|
|
530
|
+
#### Serverless / short-lived processes
|
|
531
|
+
|
|
532
|
+
In AWS Lambda (and anything else that freezes rather than exits) the rules are different, and
|
|
533
|
+
getting them wrong is silent:
|
|
534
|
+
|
|
535
|
+
- **Flush before the handler returns.** Lambda freezes the execution environment the instant
|
|
536
|
+
your handler returns, so the worker's interval-based flush stops mid-interval and whatever is
|
|
537
|
+
still queued is lost when the container is eventually reaped. `atexit` does not save you —
|
|
538
|
+
a frozen environment is killed without running exit handlers, so `flush()` is the *only*
|
|
539
|
+
guaranteed drain there.
|
|
540
|
+
- **Put it in a `finally`.** A flush written as the last line of the handler body is precisely
|
|
541
|
+
the line that does not run when the handler raises, and the invocation whose logs are most
|
|
542
|
+
worth having is the one that failed.
|
|
543
|
+
- **Never call `shutdown()` per invocation.** It is terminal: the worker does not come back, so
|
|
544
|
+
the first invocation on a warm container would log and every later one would silently log
|
|
545
|
+
nothing. That failure reads as "works locally, broken in production".
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
import log_foundry as lf
|
|
549
|
+
from log_foundry.sinks.sqs import SQSSink
|
|
550
|
+
|
|
551
|
+
lf.configure(service="billing-api", env="prod", sink=SQSSink(queue_url=QUEUE_URL))
|
|
552
|
+
|
|
553
|
+
@lf.trace
|
|
554
|
+
def handler(event, context):
|
|
555
|
+
lf.info("received", records=len(event["Records"]))
|
|
556
|
+
try:
|
|
557
|
+
return do_work(event)
|
|
558
|
+
finally:
|
|
559
|
+
lf.flush() # in `finally`: the failed invocation is the one worth logging.
|
|
560
|
+
# NEVER shutdown() here — the worker does not come back, and every
|
|
561
|
+
# later invocation on this warm container would log nothing.
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Note that each invocation is its own trace: trace context does not cross a process boundary
|
|
565
|
+
today, so N invocations produce N `trace_id`s. Correlate them with a shared
|
|
566
|
+
[`set_baggage`](#logging-inside-a-span) field until cross-process continuation ships.
|
|
508
567
|
|
|
509
568
|
## Event schema
|
|
510
569
|
|
|
@@ -170,6 +170,12 @@ never crashes the worker or the app. If the bounded queue fills completely, new
|
|
|
170
170
|
dropped (newest-first) and counted rather than blocking your code. On `shutdown()` the worker
|
|
171
171
|
stops, sweeps anything still queued into one final batch, emits it, and closes the sink.
|
|
172
172
|
|
|
173
|
+
Both triggers can be pre-empted: `flush()` puts a marker in the queue and the worker emits the
|
|
174
|
+
pending pile the moment it reaches it, ignoring the count and time triggers. Because the queue
|
|
175
|
+
is FIFO, everything submitted before the call is necessarily ahead of that marker — which is
|
|
176
|
+
exactly why the guarantee is "events submitted before this call", and why concurrent
|
|
177
|
+
submissions from other threads may or may not be included.
|
|
178
|
+
|
|
173
179
|
## Usage
|
|
174
180
|
|
|
175
181
|
### `configure(...)`
|
|
@@ -458,17 +464,70 @@ a failing sink with backoff, and applies backpressure so a slow or down sink can
|
|
|
458
464
|
back-pressure the app: when its bounded queue is full it drops the newest submissions and counts
|
|
459
465
|
them (`worker.dropped`) rather than stalling.
|
|
460
466
|
|
|
461
|
-
Because delivery is asynchronous, drain before the process exits
|
|
467
|
+
Because delivery is asynchronous, drain before the process exits. There are two drains, and
|
|
468
|
+
which one you want depends on whether the process is about to end:
|
|
462
469
|
|
|
463
470
|
```python
|
|
464
471
|
import log_foundry as lf
|
|
465
472
|
|
|
466
|
-
lf.
|
|
473
|
+
lf.flush() # drain to the sink and keep going; returns True when everything landed
|
|
474
|
+
lf.shutdown() # drain, close the sink, and stop for good; blocks until drained
|
|
467
475
|
```
|
|
468
476
|
|
|
477
|
+
| | `flush()` | `shutdown()` |
|
|
478
|
+
|---|---|---|
|
|
479
|
+
| Drains buffered events | yes | yes |
|
|
480
|
+
| Closes the sink | no | yes |
|
|
481
|
+
| Worker survives | yes | **no** — it never comes back |
|
|
482
|
+
| Repeatable | yes | idempotent, but only the first call does anything |
|
|
483
|
+
| Use it | before returning from a handler, or at a checkpoint | once, as the process exits |
|
|
484
|
+
|
|
469
485
|
`shutdown()` is also registered via `atexit`, so a normal exit flushes automatically — call it
|
|
470
|
-
explicitly when you need to be certain the tail reached the sink before a fast exit
|
|
471
|
-
end of a short script
|
|
486
|
+
explicitly when you need to be certain the tail reached the sink before a fast exit, e.g. at the
|
|
487
|
+
end of a short script. It is idempotent.
|
|
488
|
+
|
|
489
|
+
`flush(timeout=5.0)` returns `True` when every event submitted before the call has been passed
|
|
490
|
+
to the sink, and `False` if that did not happen within `timeout` (or the worker was already shut
|
|
491
|
+
down). It never raises — a logging call must not be the reason your function fails. Passing
|
|
492
|
+
`timeout=None` waits indefinitely, which is unsafe anywhere with an execution deadline.
|
|
493
|
+
|
|
494
|
+
#### Serverless / short-lived processes
|
|
495
|
+
|
|
496
|
+
In AWS Lambda (and anything else that freezes rather than exits) the rules are different, and
|
|
497
|
+
getting them wrong is silent:
|
|
498
|
+
|
|
499
|
+
- **Flush before the handler returns.** Lambda freezes the execution environment the instant
|
|
500
|
+
your handler returns, so the worker's interval-based flush stops mid-interval and whatever is
|
|
501
|
+
still queued is lost when the container is eventually reaped. `atexit` does not save you —
|
|
502
|
+
a frozen environment is killed without running exit handlers, so `flush()` is the *only*
|
|
503
|
+
guaranteed drain there.
|
|
504
|
+
- **Put it in a `finally`.** A flush written as the last line of the handler body is precisely
|
|
505
|
+
the line that does not run when the handler raises, and the invocation whose logs are most
|
|
506
|
+
worth having is the one that failed.
|
|
507
|
+
- **Never call `shutdown()` per invocation.** It is terminal: the worker does not come back, so
|
|
508
|
+
the first invocation on a warm container would log and every later one would silently log
|
|
509
|
+
nothing. That failure reads as "works locally, broken in production".
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
import log_foundry as lf
|
|
513
|
+
from log_foundry.sinks.sqs import SQSSink
|
|
514
|
+
|
|
515
|
+
lf.configure(service="billing-api", env="prod", sink=SQSSink(queue_url=QUEUE_URL))
|
|
516
|
+
|
|
517
|
+
@lf.trace
|
|
518
|
+
def handler(event, context):
|
|
519
|
+
lf.info("received", records=len(event["Records"]))
|
|
520
|
+
try:
|
|
521
|
+
return do_work(event)
|
|
522
|
+
finally:
|
|
523
|
+
lf.flush() # in `finally`: the failed invocation is the one worth logging.
|
|
524
|
+
# NEVER shutdown() here — the worker does not come back, and every
|
|
525
|
+
# later invocation on this warm container would log nothing.
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Note that each invocation is its own trace: trace context does not cross a process boundary
|
|
529
|
+
today, so N invocations produce N `trace_id`s. Correlate them with a shared
|
|
530
|
+
[`set_baggage`](#logging-inside-a-span) field until cross-process continuation ships.
|
|
472
531
|
|
|
473
532
|
## Event schema
|
|
474
533
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""log_foundry — consistent, structured logs per decorated function call.
|
|
2
|
+
|
|
3
|
+
Public façade (module-function shape). Exposes configuration, the ``@trace`` decorator, the
|
|
4
|
+
``debug/info/warning/error/critical`` emitters, ``set_baggage``, ``flush`` (drain and keep
|
|
5
|
+
logging) and ``shutdown`` (drain, close the sink, and stop).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError
|
|
9
|
+
from importlib.metadata import version as _dist_version
|
|
10
|
+
|
|
11
|
+
from log_foundry.api import critical, debug, error, info, set_baggage, warning
|
|
12
|
+
from log_foundry.config import configure, get_config
|
|
13
|
+
from log_foundry.decorator import trace
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
# Distribution name ("log-foundry") differs from the import name ("log_foundry").
|
|
17
|
+
__version__ = _dist_version("log-foundry")
|
|
18
|
+
except PackageNotFoundError: # running from a source tree that isn't installed
|
|
19
|
+
__version__ = "0.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def flush(timeout: float | None = 5.0) -> bool:
|
|
23
|
+
"""Drain buffered events through the sink without closing it.
|
|
24
|
+
|
|
25
|
+
Every event submitted before this call has been passed to ``sink.emit`` when it returns
|
|
26
|
+
``True``. (Events submitted concurrently by another thread may or may not be included —
|
|
27
|
+
the caller cannot have meant those.) Unlike :func:`shutdown` the background worker stays
|
|
28
|
+
alive and the sink stays open, so logging continues normally afterwards.
|
|
29
|
+
|
|
30
|
+
This is the drain for a process that is *frozen* rather than exited — an AWS Lambda
|
|
31
|
+
handler must drain before it returns, but will be invoked again on the same warm
|
|
32
|
+
container. Call it in a ``finally``: the invocation worth logging is the one that failed.
|
|
33
|
+
|
|
34
|
+
``timeout=None`` waits indefinitely, which is unsafe in any environment with an execution
|
|
35
|
+
deadline — it converts "some logs were lost" into "the invocation timed out".
|
|
36
|
+
|
|
37
|
+
Returns ``False`` if the drain did not complete within ``timeout``, or if the worker has
|
|
38
|
+
already been shut down. Never raises.
|
|
39
|
+
"""
|
|
40
|
+
from log_foundry.decorator import _flush_worker
|
|
41
|
+
|
|
42
|
+
return _flush_worker(timeout)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def shutdown() -> None:
|
|
46
|
+
"""Flush buffered events and close the sink, blocking until drained. Idempotent.
|
|
47
|
+
|
|
48
|
+
Also registered via ``atexit``; call it explicitly before a fast process exit when you
|
|
49
|
+
want to be certain the tail of the queue reached the sink (SPEC-004 FR-005).
|
|
50
|
+
|
|
51
|
+
This is **terminal** — the worker does not come back. Do not call it per-invocation in a
|
|
52
|
+
serverless handler: the first invocation on a warm container would log and every later one
|
|
53
|
+
would silently log nothing. Use :func:`flush` there, which drains and keeps the worker.
|
|
54
|
+
"""
|
|
55
|
+
from log_foundry.decorator import _shutdown_worker
|
|
56
|
+
|
|
57
|
+
_shutdown_worker()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"configure",
|
|
62
|
+
"get_config",
|
|
63
|
+
"trace",
|
|
64
|
+
"debug",
|
|
65
|
+
"info",
|
|
66
|
+
"warning",
|
|
67
|
+
"error",
|
|
68
|
+
"critical",
|
|
69
|
+
"set_baggage",
|
|
70
|
+
"flush",
|
|
71
|
+
"shutdown",
|
|
72
|
+
"__version__",
|
|
73
|
+
]
|
|
@@ -80,6 +80,25 @@ def _shutdown_worker() -> None:
|
|
|
80
80
|
_worker.shutdown()
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def _flush_worker(timeout: float | None = 5.0) -> bool:
|
|
84
|
+
"""Drain the process worker without retiring it. Backs ``flush()`` (SPEC-013 FR-003).
|
|
85
|
+
|
|
86
|
+
Returns ``True`` when no worker exists: a process that never logged has nothing to drain,
|
|
87
|
+
and building one here — with the thread and ``atexit`` registration :func:`_get_worker`
|
|
88
|
+
brings — in order to flush nothing would be pure cost. So this deliberately does *not* call
|
|
89
|
+
:func:`_get_worker`.
|
|
90
|
+
"""
|
|
91
|
+
worker = _worker
|
|
92
|
+
if worker is None:
|
|
93
|
+
return True
|
|
94
|
+
try:
|
|
95
|
+
return worker.flush(timeout)
|
|
96
|
+
except Exception: # noqa: BLE001 — a flush is the call most likely to be made in a
|
|
97
|
+
# `finally`, so the library must never be the reason a caller's function fails. Any
|
|
98
|
+
# failure is reported by the return value instead (FR-003).
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
83
102
|
def _flush(span: Span) -> None:
|
|
84
103
|
"""Hand the finished span's events to the background worker — non-blocking (FR-001).
|
|
85
104
|
|
|
@@ -8,6 +8,11 @@ bounded and overflow is dropped-newest with a counter (arch §9). Emit failures
|
|
|
8
8
|
with backoff; a graceful :meth:`shutdown` drains the queue, emits the tail, and closes the
|
|
9
9
|
sink so buffered events survive process exit.
|
|
10
10
|
|
|
11
|
+
Two drains, deliberately distinct (SPEC-013): :meth:`shutdown` is terminal — it stops the thread
|
|
12
|
+
and closes the sink, and the worker never comes back — while :meth:`flush` drains on demand and
|
|
13
|
+
leaves everything running. A process that is frozen rather than exited (a serverless handler
|
|
14
|
+
between invocations) needs the second, because it will be asked to log again.
|
|
15
|
+
|
|
11
16
|
This module owns *delivery mechanics only*; it receives already-built event dicts and knows
|
|
12
17
|
nothing about spans or context (the same dumbness that makes sinks swappable).
|
|
13
18
|
"""
|
|
@@ -29,6 +34,25 @@ __all__ = ["Worker"]
|
|
|
29
34
|
_SHUTDOWN = object()
|
|
30
35
|
|
|
31
36
|
|
|
37
|
+
class _FlushMarker:
|
|
38
|
+
"""A drain request travelling the queue in FIFO order (SPEC-013 FR-002).
|
|
39
|
+
|
|
40
|
+
The mechanism follows from the queue being FIFO: everything submitted *before* ``flush()``
|
|
41
|
+
was called is necessarily ahead of the marker, so by the time the worker dequeues it those
|
|
42
|
+
events are either already emitted or sitting in ``pending``. The worker emits ``pending``,
|
|
43
|
+
then sets ``event``. No lock, no inspection of queue internals, and no coordination with the
|
|
44
|
+
batching triggers.
|
|
45
|
+
|
|
46
|
+
Like ``_SHUTDOWN`` it is never emitted — but unlike ``_SHUTDOWN`` it carries state, so it is
|
|
47
|
+
a class rather than a bare sentinel object.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__slots__ = ("event",)
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self.event = threading.Event()
|
|
54
|
+
|
|
55
|
+
|
|
32
56
|
class Worker:
|
|
33
57
|
"""Owns a bounded queue + daemon thread that batches and flushes events to ``sink``."""
|
|
34
58
|
|
|
@@ -71,6 +95,36 @@ class Worker:
|
|
|
71
95
|
with self._lock:
|
|
72
96
|
self.dropped += 1
|
|
73
97
|
|
|
98
|
+
def flush(self, timeout: float | None = 5.0) -> bool:
|
|
99
|
+
"""Drain everything submitted before this call through the sink, without stopping.
|
|
100
|
+
|
|
101
|
+
Returns ``True`` once the worker has emitted them, ``False`` on timeout or when the
|
|
102
|
+
worker has already been shut down. Unlike :meth:`shutdown` the thread keeps running, the
|
|
103
|
+
sink is **not** closed, and the once-only shutdown flag is untouched, so logging
|
|
104
|
+
continues normally afterwards (SPEC-013 FR-002).
|
|
105
|
+
"""
|
|
106
|
+
with self._lock:
|
|
107
|
+
if self._shutdown_done:
|
|
108
|
+
# Nothing will ever consume a marker now, so report the failure immediately
|
|
109
|
+
# rather than make the caller wait out `timeout` for a drain that cannot happen.
|
|
110
|
+
# A caller with an execution deadline would pay that wait for nothing (FR-003).
|
|
111
|
+
return False
|
|
112
|
+
if not self._thread.is_alive():
|
|
113
|
+
return False
|
|
114
|
+
marker = _FlushMarker()
|
|
115
|
+
# One deadline shared by the put and the wait: a caller asked for a bound on the whole
|
|
116
|
+
# call, not on each half of it, so the two cannot add up to 2 * timeout.
|
|
117
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
118
|
+
try:
|
|
119
|
+
# A *blocking* put, never put_nowait: on a full queue put_nowait would skip the
|
|
120
|
+
# flush and return as though it had succeeded, which is the one outcome a flush must
|
|
121
|
+
# never produce silently. A put that times out is reported as False.
|
|
122
|
+
self._queue.put(marker, timeout=timeout)
|
|
123
|
+
except queue.Full:
|
|
124
|
+
return False
|
|
125
|
+
remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
|
|
126
|
+
return marker.event.wait(remaining)
|
|
127
|
+
|
|
74
128
|
def shutdown(self) -> None:
|
|
75
129
|
"""Stop the thread, drain + emit everything queued, then ``close()`` the sink.
|
|
76
130
|
|
|
@@ -101,6 +155,22 @@ class Worker:
|
|
|
101
155
|
item = self._queue.get(timeout=timeout)
|
|
102
156
|
except queue.Empty:
|
|
103
157
|
item = None
|
|
158
|
+
if isinstance(item, _FlushMarker):
|
|
159
|
+
# Drain on demand: emit immediately, ignoring both the batch_size and the
|
|
160
|
+
# flush_interval trigger — a caller who asked for a flush is not interested in
|
|
161
|
+
# the batching policy. Note this branch *returns to the top of the loop*: the
|
|
162
|
+
# marker must never fall through to the append below, where it would be treated
|
|
163
|
+
# as a list of events and handed to sink.emit, killing this thread.
|
|
164
|
+
try:
|
|
165
|
+
if pending:
|
|
166
|
+
self._emit(pending)
|
|
167
|
+
pending = []
|
|
168
|
+
finally:
|
|
169
|
+
# Signal even if the emit died, so a waiter is released rather than left to
|
|
170
|
+
# wait out its timeout on a thread that is no longer running.
|
|
171
|
+
last_flush = time.monotonic()
|
|
172
|
+
item.event.set()
|
|
173
|
+
continue
|
|
104
174
|
if item is not None and item is not _SHUTDOWN:
|
|
105
175
|
pending.append(cast("list[dict[str, object]]", item))
|
|
106
176
|
now = time.monotonic()
|
|
@@ -116,15 +186,24 @@ class Worker:
|
|
|
116
186
|
|
|
117
187
|
def _final_drain(self, pending: list[list[dict[str, object]]]) -> None:
|
|
118
188
|
"""On stop, pull anything still queued and emit the tail as one final batch."""
|
|
189
|
+
markers: list[_FlushMarker] = []
|
|
119
190
|
while True:
|
|
120
191
|
try:
|
|
121
192
|
item = self._queue.get_nowait()
|
|
122
193
|
except queue.Empty:
|
|
123
194
|
break
|
|
195
|
+
if isinstance(item, _FlushMarker):
|
|
196
|
+
# This guard is a second copy of _run's and needs the same exclusion. Markers
|
|
197
|
+
# are answered *after* the final emit below, so a flush() that raced shutdown()
|
|
198
|
+
# still returns True — and truthfully: its events really did reach the sink.
|
|
199
|
+
markers.append(item)
|
|
200
|
+
continue
|
|
124
201
|
if item is not None and item is not _SHUTDOWN:
|
|
125
202
|
pending.append(cast("list[dict[str, object]]", item))
|
|
126
203
|
if pending:
|
|
127
204
|
self._emit(pending)
|
|
205
|
+
for marker in markers:
|
|
206
|
+
marker.event.set()
|
|
128
207
|
|
|
129
208
|
def _emit(self, event_lists: list[list[dict[str, object]]]) -> None:
|
|
130
209
|
"""Flatten queued per-span event-lists into one batch and emit, retrying with backoff.
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""log_foundry — consistent, structured logs per decorated function call.
|
|
2
|
-
|
|
3
|
-
Public façade (module-function shape). Exposes configuration, the ``@trace`` decorator, the
|
|
4
|
-
``debug/info/warning/error/critical`` emitters, ``set_baggage``, and ``shutdown`` (graceful
|
|
5
|
-
drain of the background worker).
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from importlib.metadata import PackageNotFoundError
|
|
9
|
-
from importlib.metadata import version as _dist_version
|
|
10
|
-
|
|
11
|
-
from log_foundry.api import critical, debug, error, info, set_baggage, warning
|
|
12
|
-
from log_foundry.config import configure, get_config
|
|
13
|
-
from log_foundry.decorator import trace
|
|
14
|
-
|
|
15
|
-
try:
|
|
16
|
-
# Distribution name ("log-foundry") differs from the import name ("log_foundry").
|
|
17
|
-
__version__ = _dist_version("log-foundry")
|
|
18
|
-
except PackageNotFoundError: # running from a source tree that isn't installed
|
|
19
|
-
__version__ = "0.0.0"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def shutdown() -> None:
|
|
23
|
-
"""Flush buffered events and close the sink, blocking until drained. Idempotent.
|
|
24
|
-
|
|
25
|
-
Also registered via ``atexit``; call it explicitly before a fast process exit when you
|
|
26
|
-
want to be certain the tail of the queue reached the sink (SPEC-004 FR-005).
|
|
27
|
-
"""
|
|
28
|
-
from log_foundry.decorator import _shutdown_worker
|
|
29
|
-
|
|
30
|
-
_shutdown_worker()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
__all__ = [
|
|
34
|
-
"configure",
|
|
35
|
-
"get_config",
|
|
36
|
-
"trace",
|
|
37
|
-
"debug",
|
|
38
|
-
"info",
|
|
39
|
-
"warning",
|
|
40
|
-
"error",
|
|
41
|
-
"critical",
|
|
42
|
-
"set_baggage",
|
|
43
|
-
"shutdown",
|
|
44
|
-
"__version__",
|
|
45
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|