log-foundry 0.1.1.dev1__tar.gz → 0.1.1.dev2__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.1.1.dev1 → log_foundry-0.1.1.dev2}/PKG-INFO +75 -66
- {log_foundry-0.1.1.dev1 → log_foundry-0.1.1.dev2}/README.md +74 -65
- {log_foundry-0.1.1.dev1 → log_foundry-0.1.1.dev2}/pyproject.toml +7 -6
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/__init__.py +6 -6
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/api.py +7 -7
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/config.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/console.py +1 -1
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/context.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/decorator.py +5 -5
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/model.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/_socket.py +1 -1
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/callback.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/clickhouse.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/datadog.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/elasticsearch.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/eventhubs.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/file.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/filtering.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/firehose.py +5 -5
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/honeycomb.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/http.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/kafka.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/kinesis.py +5 -5
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/logging_sink.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/logstash.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/loki.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/mongodb.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/multi.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/nats.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/newrelic.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/postgres.py +3 -3
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/pubsub.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/rabbitmq.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/redis.py +1 -1
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/sentry.py +7 -7
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/sns.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/splunk.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/sqlite.py +1 -1
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/sqs.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/syslog.py +5 -5
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/transform.py +2 -2
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/util.py +4 -4
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/worker.py +3 -3
- {log_foundry-0.1.1.dev1 → log_foundry-0.1.1.dev2}/LICENSE +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/ids.py +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/py.typed +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/__init__.py +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/_chunk.py +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/_time.py +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/base.py +0 -0
- {log_foundry-0.1.1.dev1/src/log_forge → log_foundry-0.1.1.dev2/src/log_foundry}/sinks/stdout.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: log-foundry
|
|
3
|
-
Version: 0.1.1.
|
|
3
|
+
Version: 0.1.1.dev2
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -33,13 +33,13 @@ Requires-Dist: redis (>=5.0) ; extra == "redis"
|
|
|
33
33
|
Requires-Dist: sentry-sdk (>=2.0) ; extra == "sentry"
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
|
|
36
|
-
# log-
|
|
36
|
+
# log-foundry
|
|
37
37
|
|
|
38
38
|
Consistent, structured (JSON) logs for every decorated function call — correlated by shared
|
|
39
39
|
trace/span IDs, ready to ship to any of 30-plus built-in sinks (stdout by default; SQS → ELK is
|
|
40
40
|
the headline production path).
|
|
41
41
|
|
|
42
|
-
`log-
|
|
42
|
+
`log-foundry` owns the **logs** pillar of observability. You decorate a function with `@trace`;
|
|
43
43
|
it emits one identically-shaped JSON record when the call starts and another when it ends
|
|
44
44
|
(with duration and status), stitched together by W3C-compatible trace and span IDs so nested
|
|
45
45
|
calls form a tree you can query later.
|
|
@@ -67,16 +67,25 @@ pip install log-foundry # core, zero dependencies
|
|
|
67
67
|
pip install 'log-foundry[aws]' # + boto3 for the SQS/SNS/Kinesis/Firehose sinks
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
> **
|
|
71
|
-
>
|
|
70
|
+
> **Renamed in 0.2.0: `log_forge` → `log_foundry`.** The import package now matches the
|
|
71
|
+
> distribution name — `pip install log-foundry`, then `import log_foundry`. If you are on
|
|
72
|
+
> `0.1.x`, update your imports; there is no compatibility shim. The project was originally
|
|
73
|
+
> called *log-forge*, but PyPI rejects that name as too similar to the unrelated, pre-existing
|
|
72
74
|
> [`logforge`](https://pypi.org/project/logforge/) project — its similarity check collapses
|
|
73
|
-
> separators, so `log-forge` and `logforge` count as the same name.
|
|
74
|
-
>
|
|
75
|
+
> separators, so `log-forge` and `logforge` count as the same name. Rather than keep a
|
|
76
|
+
> distribution and an import name that disagreed, everything is now `log-foundry` /
|
|
77
|
+
> `log_foundry`.
|
|
78
|
+
>
|
|
79
|
+
> Migrating from `0.1.x` is a find-and-replace on `log_forge` → `log_foundry`; no module moved
|
|
80
|
+
> and no public API changed. A handful of *emitted* defaults carry the name and shift with it:
|
|
81
|
+
> `LoggingSink`'s default logger (`logging.getLogger("log_foundry")`), `SyslogSink(app_name=…)`,
|
|
82
|
+
> `SplunkHECSink(source=…)`, Datadog's `ddsource`, and Sentry's client tag. Override them
|
|
83
|
+
> explicitly if a downstream query or dashboard pins the old string.
|
|
75
84
|
|
|
76
85
|
```python
|
|
77
|
-
import
|
|
86
|
+
import log_foundry
|
|
78
87
|
|
|
79
|
-
print(
|
|
88
|
+
print(log_foundry.__version__) # the installed version
|
|
80
89
|
```
|
|
81
90
|
|
|
82
91
|
To work on the library itself, install from a clone:
|
|
@@ -111,7 +120,7 @@ Loki, Logstash, Syslog, Datadog, Splunk, New Relic, Honeycomb) — need **no** e
|
|
|
111
120
|
## Quickstart
|
|
112
121
|
|
|
113
122
|
```python
|
|
114
|
-
import
|
|
123
|
+
import log_foundry as lf
|
|
115
124
|
|
|
116
125
|
# Call once at startup. These values are stamped onto every event.
|
|
117
126
|
lf.configure(service="billing-api", version="1.4.2", env="prod")
|
|
@@ -143,7 +152,7 @@ with the child span pointing at its parent via `parent_span_id`:
|
|
|
143
152
|
|
|
144
153
|
## How it works
|
|
145
154
|
|
|
146
|
-

|
|
147
156
|
|
|
148
157
|
A traced call travels through a small pipeline. The first four steps run on your own thread
|
|
149
158
|
and are deliberately fast; the last two run on a background thread so your code never waits on
|
|
@@ -295,13 +304,13 @@ class Sink(Protocol):
|
|
|
295
304
|
|
|
296
305
|
Wire one up by passing an instance to `configure(sink=...)`; if you never do, the first decorated
|
|
297
306
|
call falls back to `StdoutSink()`. Sinks are **not** re-exported at the top level — import each
|
|
298
|
-
from its own module, e.g. `from
|
|
307
|
+
from its own module, e.g. `from log_foundry.sinks.sqs import SQSSink`.
|
|
299
308
|
|
|
300
309
|
A few conventions hold across every sink below:
|
|
301
310
|
|
|
302
311
|
- **Extras.** The core is dependency-free. A sink built on a third-party client sits behind the
|
|
303
312
|
optional extra named in its table (blank = zero-dependency, stdlib only); the client is imported
|
|
304
|
-
lazily, so `import
|
|
313
|
+
lazily, so `import log_foundry.sinks.<x>` never fails for a missing dependency — only *constructing*
|
|
305
314
|
the sink without an injected client does. See [Optional extras](#optional-extras).
|
|
306
315
|
- **Injection.** Sinks backed by an external resource accept an injected client/connection/stream
|
|
307
316
|
(`client=`, `connection=`, `producer=`, `stream=`, `opener=`) for testing or bespoke configuration.
|
|
@@ -315,13 +324,13 @@ A few conventions hold across every sink below:
|
|
|
315
324
|
|
|
316
325
|
| Sink | Import from | Configure |
|
|
317
326
|
|---|---|---|
|
|
318
|
-
| `StdoutSink` | `
|
|
319
|
-
| `StderrSink` | `
|
|
320
|
-
| `NullSink` | `
|
|
321
|
-
| `MemorySink` | `
|
|
327
|
+
| `StdoutSink` | `log_foundry.sinks.stdout` | `StdoutSink(stream=sys.stdout)` — one JSON line per event; the zero-config default |
|
|
328
|
+
| `StderrSink` | `log_foundry.sinks.util` | `StderrSink(stream=sys.stderr)` — same, on stderr (twelve-factor) |
|
|
329
|
+
| `NullSink` | `log_foundry.sinks.util` | `NullSink()` — discard everything; `.dropped` counts events |
|
|
330
|
+
| `MemorySink` | `log_foundry.sinks.util` | `MemorySink(maxlen=None)` — collect into `.events` (a bounded ring when `maxlen` is set) |
|
|
322
331
|
|
|
323
332
|
```python
|
|
324
|
-
from
|
|
333
|
+
from log_foundry.sinks.stdout import StdoutSink
|
|
325
334
|
lf.configure(sink=StdoutSink()) # explicit; also the zero-config default
|
|
326
335
|
```
|
|
327
336
|
|
|
@@ -332,16 +341,16 @@ to a plain callable.
|
|
|
332
341
|
|
|
333
342
|
| Sink | Import from | Configure |
|
|
334
343
|
|---|---|---|
|
|
335
|
-
| `MultiSink` | `
|
|
336
|
-
| `FilteringSink` | `
|
|
337
|
-
| `TransformSink` | `
|
|
338
|
-
| `CallbackSink` | `
|
|
344
|
+
| `MultiSink` | `log_foundry.sinks.multi` | `MultiSink(*sinks)` — forward each batch to every child; a failing child is isolated and counted on `.failed` |
|
|
345
|
+
| `FilteringSink` | `log_foundry.sinks.filtering` | `FilteringSink(inner, *, predicate=None, min_level=None)` — forward only events passing `predicate` and/or at/above `min_level` |
|
|
346
|
+
| `TransformSink` | `log_foundry.sinks.transform` | `TransformSink(inner, fn)` — map each event through `fn` before forwarding; return `None` to drop one |
|
|
347
|
+
| `CallbackSink` | `log_foundry.sinks.callback` | `CallbackSink(fn, *, on_close=None)` — hand each batch to any callable |
|
|
339
348
|
|
|
340
349
|
```python
|
|
341
|
-
from
|
|
342
|
-
from
|
|
343
|
-
from
|
|
344
|
-
from
|
|
350
|
+
from log_foundry.sinks.multi import MultiSink
|
|
351
|
+
from log_foundry.sinks.filtering import FilteringSink
|
|
352
|
+
from log_foundry.sinks.stdout import StdoutSink
|
|
353
|
+
from log_foundry.sinks.sqs import SQSSink
|
|
345
354
|
|
|
346
355
|
lf.configure(sink=MultiSink(
|
|
347
356
|
StdoutSink(), # echo everything locally
|
|
@@ -356,9 +365,9 @@ level is unknown or missing fails open (is forwarded).
|
|
|
356
365
|
|
|
357
366
|
| Sink | Import from | Configure |
|
|
358
367
|
|---|---|---|
|
|
359
|
-
| `LoggingSink` | `
|
|
368
|
+
| `LoggingSink` | `log_foundry.sinks.logging_sink` | `LoggingSink(logger=None, *, default_level="INFO")` — emit each event as a `logging.LogRecord` |
|
|
360
369
|
|
|
361
|
-
Hands every event to a `logging.Logger` (default `logging.getLogger("
|
|
370
|
+
Hands every event to a `logging.Logger` (default `logging.getLogger("log_foundry")`) so your existing
|
|
362
371
|
handlers, formatters, and `logging.config` apply. Identity fields and the nested `fields` are
|
|
363
372
|
attached to each record; the sink never configures or tears down logging itself.
|
|
364
373
|
|
|
@@ -366,9 +375,9 @@ attached to each record; the sink never configures or tears down logging itself.
|
|
|
366
375
|
|
|
367
376
|
| Sink | Import from | Configure |
|
|
368
377
|
|---|---|---|
|
|
369
|
-
| `FileSink` | `
|
|
370
|
-
| `RotatingFileSink` | `
|
|
371
|
-
| `SQLiteSink` | `
|
|
378
|
+
| `FileSink` | `log_foundry.sinks.file` | `FileSink(path, *, encoding="utf-8")` — append NDJSON to one file |
|
|
379
|
+
| `RotatingFileSink` | `log_foundry.sinks.file` | `RotatingFileSink(path, *, max_bytes=0, backup_count=0, when=None, interval=1)` — rotate by size and/or time, keeping `backup_count` numbered backups |
|
|
380
|
+
| `SQLiteSink` | `log_foundry.sinks.sqlite` | `SQLiteSink(database, *, table="log_events", create_table=True)` — batch-insert into an embedded SQLite DB |
|
|
372
381
|
|
|
373
382
|
`RotatingFileSink`'s time trigger uses a `when` unit code — `"S"`/`"M"`/`"H"`/`"D"` — times `interval`
|
|
374
383
|
(either trigger, or both, can be enabled). `SQLiteSink` stores each event as full JSON plus projected
|
|
@@ -376,7 +385,7 @@ attached to each record; the sink never configures or tears down logging itself.
|
|
|
376
385
|
you provision the table yourself.
|
|
377
386
|
|
|
378
387
|
```python
|
|
379
|
-
from
|
|
388
|
+
from log_foundry.sinks.file import RotatingFileSink
|
|
380
389
|
lf.configure(sink=RotatingFileSink("app.log.jsonl", max_bytes=10_000_000, backup_count=5))
|
|
381
390
|
```
|
|
382
391
|
|
|
@@ -388,15 +397,15 @@ to `HTTPSink` (`headers=`, `auth=`, `gzip=`, `timeout=`, `max_retries=`).
|
|
|
388
397
|
|
|
389
398
|
| Sink | Import from | Configure |
|
|
390
399
|
|---|---|---|
|
|
391
|
-
| `HTTPSink` | `
|
|
392
|
-
| `ElasticsearchSink` | `
|
|
393
|
-
| `OpenSearchSink` | `
|
|
394
|
-
| `LokiSink` | `
|
|
395
|
-
| `LogstashSink` | `
|
|
396
|
-
| `SyslogSink` | `
|
|
400
|
+
| `HTTPSink` | `log_foundry.sinks.http` | `HTTPSink(url, *, method="POST", headers=None, auth=None, body_format="ndjson", timeout=5.0, gzip=False, max_retries=3)` — generic POST. `auth` is a bearer-token `str` or `(user, pass)` for basic; `body_format` is `"ndjson"` or `"json_array"` |
|
|
401
|
+
| `ElasticsearchSink` | `log_foundry.sinks.elasticsearch` | `ElasticsearchSink(url, *, index, auth=None, **http_kwargs)` — POST to `_bulk`, parsing per-item errors (`.item_errors`) |
|
|
402
|
+
| `OpenSearchSink` | `log_foundry.sinks.elasticsearch` | same signature as `ElasticsearchSink` (identical bulk protocol) |
|
|
403
|
+
| `LokiSink` | `log_foundry.sinks.loki` | `LokiSink(url, *, labels=("service", "env", "level"), **http_kwargs)` — Grafana Loki push API |
|
|
404
|
+
| `LogstashSink` | `log_foundry.sinks.logstash` | `LogstashSink(url=…, **http_kwargs)` for HTTP, **or** `LogstashSink(host=…, port=…, transport="tcp")` for a raw TCP/UDP socket |
|
|
405
|
+
| `SyslogSink` | `log_foundry.sinks.syslog` | `SyslogSink(host, port=514, *, transport="udp", facility="user", app_name="log-foundry")` — RFC 5424 over UDP/TCP |
|
|
397
406
|
|
|
398
407
|
```python
|
|
399
|
-
from
|
|
408
|
+
from log_foundry.sinks.elasticsearch import ElasticsearchSink
|
|
400
409
|
lf.configure(sink=ElasticsearchSink("https://es.internal:9200", index="app-logs",
|
|
401
410
|
auth=("elastic", "…")))
|
|
402
411
|
```
|
|
@@ -408,11 +417,11 @@ Also HTTP-based. All are zero-dependency **except** `SentrySink`, which prefers
|
|
|
408
417
|
|
|
409
418
|
| Sink | Import from | Extra | Configure |
|
|
410
419
|
|---|---|---|---|
|
|
411
|
-
| `DatadogSink` | `
|
|
412
|
-
| `SplunkHECSink` | `
|
|
413
|
-
| `NewRelicSink` | `
|
|
414
|
-
| `HoneycombSink` | `
|
|
415
|
-
| `SentrySink` | `
|
|
420
|
+
| `DatadogSink` | `log_foundry.sinks.datadog` | — | `DatadogSink(api_key, *, site="datadoghq.com", service=None, ddtags=None)` |
|
|
421
|
+
| `SplunkHECSink` | `log_foundry.sinks.splunk` | — | `SplunkHECSink(url, token, *, host=None, source="log-foundry")` — HTTP Event Collector |
|
|
422
|
+
| `NewRelicSink` | `log_foundry.sinks.newrelic` | — | `NewRelicSink(api_key, *, region="US")` — `region` is `"US"` or `"EU"` |
|
|
423
|
+
| `HoneycombSink` | `log_foundry.sinks.honeycomb` | — | `HoneycombSink(api_key, dataset, *, url="https://api.honeycomb.io")` |
|
|
424
|
+
| `SentrySink` | `log_foundry.sinks.sentry` | `sentry` | `SentrySink(dsn=None, *, min_level="ERROR")` — sends only `min_level`+ events |
|
|
416
425
|
|
|
417
426
|
With the `sentry` extra installed, `SentrySink` captures via `sentry_sdk.capture_event` (initialize
|
|
418
427
|
the SDK yourself with `sentry_sdk.init(...)`); without it, pass `dsn=` and events are POSTed as
|
|
@@ -421,19 +430,19 @@ Sentry envelopes over HTTP.
|
|
|
421
430
|
#### AWS — the durable-buffer path (`aws` extra)
|
|
422
431
|
|
|
423
432
|
`pip install 'log-foundry[aws]'` (pulls `boto3`). Credentials and region come from boto3's standard
|
|
424
|
-
chain — log-
|
|
433
|
+
chain — log-foundry adds none of its own. Each re-chunks every batch to the service's hard per-request
|
|
425
434
|
limits, retries partial failures, and drops any single event too large to ever fit (counted on
|
|
426
435
|
`.dropped_oversized`).
|
|
427
436
|
|
|
428
437
|
| Sink | Import from | Configure |
|
|
429
438
|
|---|---|---|
|
|
430
|
-
| `SQSSink` | `
|
|
431
|
-
| `SNSSink` | `
|
|
432
|
-
| `KinesisSink` | `
|
|
433
|
-
| `FirehoseSink` | `
|
|
439
|
+
| `SQSSink` | `log_foundry.sinks.sqs` | `SQSSink(queue_url, *, max_retries=3)` — the headline production path: a durable buffer in front of ELK, absorbing downstream spikes/outages |
|
|
440
|
+
| `SNSSink` | `log_foundry.sinks.sns` | `SNSSink(topic_arn, *, max_retries=3)` |
|
|
441
|
+
| `KinesisSink` | `log_foundry.sinks.kinesis` | `KinesisSink(stream_name, *, partition_key_field="trace_id", max_retries=3)` |
|
|
442
|
+
| `FirehoseSink` | `log_foundry.sinks.firehose` | `FirehoseSink(delivery_stream, *, max_retries=3)` |
|
|
434
443
|
|
|
435
444
|
```python
|
|
436
|
-
from
|
|
445
|
+
from log_foundry.sinks.sqs import SQSSink
|
|
437
446
|
lf.configure(service="payments",
|
|
438
447
|
sink=SQSSink(queue_url="https://sqs.us-east-1.amazonaws.com/123456789012/logs"))
|
|
439
448
|
```
|
|
@@ -446,16 +455,16 @@ Each needs its own extra (lazy-imported). All publish + retry within a bound and
|
|
|
446
455
|
|
|
447
456
|
| Sink | Import from | Extra | Configure |
|
|
448
457
|
|---|---|---|---|
|
|
449
|
-
| `KafkaSink` | `
|
|
450
|
-
| `RedisStreamsSink` | `
|
|
451
|
-
| `RedisListSink` | `
|
|
452
|
-
| `RabbitMQSink` | `
|
|
453
|
-
| `NATSSink` | `
|
|
454
|
-
| `GooglePubSubSink` | `
|
|
455
|
-
| `AzureEventHubsSink` | `
|
|
458
|
+
| `KafkaSink` | `log_foundry.sinks.kafka` | `kafka` | `KafkaSink(topic, *, bootstrap_servers="…", key_field="trace_id")` |
|
|
459
|
+
| `RedisStreamsSink` | `log_foundry.sinks.redis` | `redis` | `RedisStreamsSink(stream, *, url=None)` — `XADD` |
|
|
460
|
+
| `RedisListSink` | `log_foundry.sinks.redis` | `redis` | `RedisListSink(key, *, url=None)` — `RPUSH` |
|
|
461
|
+
| `RabbitMQSink` | `log_foundry.sinks.rabbitmq` | `amqp` | `RabbitMQSink(*, exchange, routing_key, url=None)` — persistent messages |
|
|
462
|
+
| `NATSSink` | `log_foundry.sinks.nats` | `nats` | `NATSSink(subject, *, jetstream=False, servers=None)` |
|
|
463
|
+
| `GooglePubSubSink` | `log_foundry.sinks.pubsub` | `gcp-pubsub` | `GooglePubSubSink(topic)` |
|
|
464
|
+
| `AzureEventHubsSink` | `log_foundry.sinks.eventhubs` | `azure-eventhubs` | `AzureEventHubsSink(*, connection_str="…", eventhub=None)` |
|
|
456
465
|
|
|
457
466
|
```python
|
|
458
|
-
from
|
|
467
|
+
from log_foundry.sinks.kafka import KafkaSink
|
|
459
468
|
lf.configure(sink=KafkaSink("app-logs", bootstrap_servers="broker:9092"))
|
|
460
469
|
```
|
|
461
470
|
|
|
@@ -465,9 +474,9 @@ Write-only inserts (querying is the downstream tool's job); each needs its own e
|
|
|
465
474
|
|
|
466
475
|
| Sink | Import from | Extra | Configure |
|
|
467
476
|
|---|---|---|---|
|
|
468
|
-
| `MongoDBSink` | `
|
|
469
|
-
| `PostgresSink` | `
|
|
470
|
-
| `ClickHouseSink` | `
|
|
477
|
+
| `MongoDBSink` | `log_foundry.sinks.mongodb` | `mongo` | `MongoDBSink(*, uri="…", database="…", collection="…")` |
|
|
478
|
+
| `PostgresSink` | `log_foundry.sinks.postgres` | `postgres` | `PostgresSink(table, *, dsn="…", create_table=False)` — JSONB `event` column + extracted columns |
|
|
479
|
+
| `ClickHouseSink` | `log_foundry.sinks.clickhouse` | `clickhouse` | `ClickHouseSink(table, *, dsn="…", create_table=False)` — MergeTree, columnar insert |
|
|
471
480
|
|
|
472
481
|
`PostgresSink` / `ClickHouseSink` default `create_table=False` (you own the schema and indexes); set
|
|
473
482
|
it `True` for an idempotent `CREATE TABLE IF NOT EXISTS` convenience.
|
|
@@ -487,7 +496,7 @@ them (`worker.dropped`) rather than stalling.
|
|
|
487
496
|
Because delivery is asynchronous, drain before the process exits:
|
|
488
497
|
|
|
489
498
|
```python
|
|
490
|
-
import
|
|
499
|
+
import log_foundry as lf
|
|
491
500
|
|
|
492
501
|
lf.shutdown() # flush buffered events and close the sink; blocks until drained
|
|
493
502
|
```
|
|
@@ -532,7 +541,7 @@ poetry run mypy # typecheck (strict, over src/)
|
|
|
532
541
|
every pull request and on push to `main`. A second workflow
|
|
533
542
|
([`spec-lint.yml`](.github/workflows/spec-lint.yml)) lints the design specs under `docs/specs/`.
|
|
534
543
|
|
|
535
|
-
The library uses a src layout (`src/
|
|
544
|
+
The library uses a src layout (`src/log_foundry/`) with a single concept per module: `config`,
|
|
536
545
|
`ids`, `model`, `context`, `decorator`, `api`, `console`, `worker`, and the `sinks/` package (the
|
|
537
546
|
`base` protocol, `stdout`, and one module per sink family — see [Sinks](#sinks)).
|
|
538
547
|
Deeper design docs live in [`docs/`](docs/) — start with [`docs/architecture.md`](docs/architecture.md).
|
|
@@ -548,8 +557,8 @@ sdist and a wheel:
|
|
|
548
557
|
|
|
549
558
|
| Trigger | Version built | Published to PyPI as |
|
|
550
559
|
|---|---|---|
|
|
551
|
-
| merge to `main` | `
|
|
552
|
-
| push tag `
|
|
560
|
+
| merge to `main` | `X.Y.Z.devN` | dev pre-release |
|
|
561
|
+
| push tag `vX.Y.Z` | `X.Y.Z` | stable release |
|
|
553
562
|
|
|
554
563
|
Dev pre-releases keep the upload path exercised on every merge, so a real release is never the
|
|
555
564
|
first time it runs. `pip install log-foundry` still resolves to the latest **stable** version —
|
|
@@ -558,7 +567,7 @@ pip ignores pre-releases unless you pass `--pre`.
|
|
|
558
567
|
Cutting a release is one tag:
|
|
559
568
|
|
|
560
569
|
```bash
|
|
561
|
-
git tag -a v0.2.0 -m "log-
|
|
570
|
+
git tag -a v0.2.0 -m "log-foundry 0.2.0"
|
|
562
571
|
git push origin v0.2.0
|
|
563
572
|
```
|
|
564
573
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# log-
|
|
1
|
+
# log-foundry
|
|
2
2
|
|
|
3
3
|
Consistent, structured (JSON) logs for every decorated function call — correlated by shared
|
|
4
4
|
trace/span IDs, ready to ship to any of 30-plus built-in sinks (stdout by default; SQS → ELK is
|
|
5
5
|
the headline production path).
|
|
6
6
|
|
|
7
|
-
`log-
|
|
7
|
+
`log-foundry` owns the **logs** pillar of observability. You decorate a function with `@trace`;
|
|
8
8
|
it emits one identically-shaped JSON record when the call starts and another when it ends
|
|
9
9
|
(with duration and status), stitched together by W3C-compatible trace and span IDs so nested
|
|
10
10
|
calls form a tree you can query later.
|
|
@@ -32,16 +32,25 @@ pip install log-foundry # core, zero dependencies
|
|
|
32
32
|
pip install 'log-foundry[aws]' # + boto3 for the SQS/SNS/Kinesis/Firehose sinks
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
> **
|
|
36
|
-
>
|
|
35
|
+
> **Renamed in 0.2.0: `log_forge` → `log_foundry`.** The import package now matches the
|
|
36
|
+
> distribution name — `pip install log-foundry`, then `import log_foundry`. If you are on
|
|
37
|
+
> `0.1.x`, update your imports; there is no compatibility shim. The project was originally
|
|
38
|
+
> called *log-forge*, but PyPI rejects that name as too similar to the unrelated, pre-existing
|
|
37
39
|
> [`logforge`](https://pypi.org/project/logforge/) project — its similarity check collapses
|
|
38
|
-
> separators, so `log-forge` and `logforge` count as the same name.
|
|
39
|
-
>
|
|
40
|
+
> separators, so `log-forge` and `logforge` count as the same name. Rather than keep a
|
|
41
|
+
> distribution and an import name that disagreed, everything is now `log-foundry` /
|
|
42
|
+
> `log_foundry`.
|
|
43
|
+
>
|
|
44
|
+
> Migrating from `0.1.x` is a find-and-replace on `log_forge` → `log_foundry`; no module moved
|
|
45
|
+
> and no public API changed. A handful of *emitted* defaults carry the name and shift with it:
|
|
46
|
+
> `LoggingSink`'s default logger (`logging.getLogger("log_foundry")`), `SyslogSink(app_name=…)`,
|
|
47
|
+
> `SplunkHECSink(source=…)`, Datadog's `ddsource`, and Sentry's client tag. Override them
|
|
48
|
+
> explicitly if a downstream query or dashboard pins the old string.
|
|
40
49
|
|
|
41
50
|
```python
|
|
42
|
-
import
|
|
51
|
+
import log_foundry
|
|
43
52
|
|
|
44
|
-
print(
|
|
53
|
+
print(log_foundry.__version__) # the installed version
|
|
45
54
|
```
|
|
46
55
|
|
|
47
56
|
To work on the library itself, install from a clone:
|
|
@@ -76,7 +85,7 @@ Loki, Logstash, Syslog, Datadog, Splunk, New Relic, Honeycomb) — need **no** e
|
|
|
76
85
|
## Quickstart
|
|
77
86
|
|
|
78
87
|
```python
|
|
79
|
-
import
|
|
88
|
+
import log_foundry as lf
|
|
80
89
|
|
|
81
90
|
# Call once at startup. These values are stamped onto every event.
|
|
82
91
|
lf.configure(service="billing-api", version="1.4.2", env="prod")
|
|
@@ -108,7 +117,7 @@ with the child span pointing at its parent via `parent_span_id`:
|
|
|
108
117
|
|
|
109
118
|
## How it works
|
|
110
119
|
|
|
111
|
-

|
|
112
121
|
|
|
113
122
|
A traced call travels through a small pipeline. The first four steps run on your own thread
|
|
114
123
|
and are deliberately fast; the last two run on a background thread so your code never waits on
|
|
@@ -260,13 +269,13 @@ class Sink(Protocol):
|
|
|
260
269
|
|
|
261
270
|
Wire one up by passing an instance to `configure(sink=...)`; if you never do, the first decorated
|
|
262
271
|
call falls back to `StdoutSink()`. Sinks are **not** re-exported at the top level — import each
|
|
263
|
-
from its own module, e.g. `from
|
|
272
|
+
from its own module, e.g. `from log_foundry.sinks.sqs import SQSSink`.
|
|
264
273
|
|
|
265
274
|
A few conventions hold across every sink below:
|
|
266
275
|
|
|
267
276
|
- **Extras.** The core is dependency-free. A sink built on a third-party client sits behind the
|
|
268
277
|
optional extra named in its table (blank = zero-dependency, stdlib only); the client is imported
|
|
269
|
-
lazily, so `import
|
|
278
|
+
lazily, so `import log_foundry.sinks.<x>` never fails for a missing dependency — only *constructing*
|
|
270
279
|
the sink without an injected client does. See [Optional extras](#optional-extras).
|
|
271
280
|
- **Injection.** Sinks backed by an external resource accept an injected client/connection/stream
|
|
272
281
|
(`client=`, `connection=`, `producer=`, `stream=`, `opener=`) for testing or bespoke configuration.
|
|
@@ -280,13 +289,13 @@ A few conventions hold across every sink below:
|
|
|
280
289
|
|
|
281
290
|
| Sink | Import from | Configure |
|
|
282
291
|
|---|---|---|
|
|
283
|
-
| `StdoutSink` | `
|
|
284
|
-
| `StderrSink` | `
|
|
285
|
-
| `NullSink` | `
|
|
286
|
-
| `MemorySink` | `
|
|
292
|
+
| `StdoutSink` | `log_foundry.sinks.stdout` | `StdoutSink(stream=sys.stdout)` — one JSON line per event; the zero-config default |
|
|
293
|
+
| `StderrSink` | `log_foundry.sinks.util` | `StderrSink(stream=sys.stderr)` — same, on stderr (twelve-factor) |
|
|
294
|
+
| `NullSink` | `log_foundry.sinks.util` | `NullSink()` — discard everything; `.dropped` counts events |
|
|
295
|
+
| `MemorySink` | `log_foundry.sinks.util` | `MemorySink(maxlen=None)` — collect into `.events` (a bounded ring when `maxlen` is set) |
|
|
287
296
|
|
|
288
297
|
```python
|
|
289
|
-
from
|
|
298
|
+
from log_foundry.sinks.stdout import StdoutSink
|
|
290
299
|
lf.configure(sink=StdoutSink()) # explicit; also the zero-config default
|
|
291
300
|
```
|
|
292
301
|
|
|
@@ -297,16 +306,16 @@ to a plain callable.
|
|
|
297
306
|
|
|
298
307
|
| Sink | Import from | Configure |
|
|
299
308
|
|---|---|---|
|
|
300
|
-
| `MultiSink` | `
|
|
301
|
-
| `FilteringSink` | `
|
|
302
|
-
| `TransformSink` | `
|
|
303
|
-
| `CallbackSink` | `
|
|
309
|
+
| `MultiSink` | `log_foundry.sinks.multi` | `MultiSink(*sinks)` — forward each batch to every child; a failing child is isolated and counted on `.failed` |
|
|
310
|
+
| `FilteringSink` | `log_foundry.sinks.filtering` | `FilteringSink(inner, *, predicate=None, min_level=None)` — forward only events passing `predicate` and/or at/above `min_level` |
|
|
311
|
+
| `TransformSink` | `log_foundry.sinks.transform` | `TransformSink(inner, fn)` — map each event through `fn` before forwarding; return `None` to drop one |
|
|
312
|
+
| `CallbackSink` | `log_foundry.sinks.callback` | `CallbackSink(fn, *, on_close=None)` — hand each batch to any callable |
|
|
304
313
|
|
|
305
314
|
```python
|
|
306
|
-
from
|
|
307
|
-
from
|
|
308
|
-
from
|
|
309
|
-
from
|
|
315
|
+
from log_foundry.sinks.multi import MultiSink
|
|
316
|
+
from log_foundry.sinks.filtering import FilteringSink
|
|
317
|
+
from log_foundry.sinks.stdout import StdoutSink
|
|
318
|
+
from log_foundry.sinks.sqs import SQSSink
|
|
310
319
|
|
|
311
320
|
lf.configure(sink=MultiSink(
|
|
312
321
|
StdoutSink(), # echo everything locally
|
|
@@ -321,9 +330,9 @@ level is unknown or missing fails open (is forwarded).
|
|
|
321
330
|
|
|
322
331
|
| Sink | Import from | Configure |
|
|
323
332
|
|---|---|---|
|
|
324
|
-
| `LoggingSink` | `
|
|
333
|
+
| `LoggingSink` | `log_foundry.sinks.logging_sink` | `LoggingSink(logger=None, *, default_level="INFO")` — emit each event as a `logging.LogRecord` |
|
|
325
334
|
|
|
326
|
-
Hands every event to a `logging.Logger` (default `logging.getLogger("
|
|
335
|
+
Hands every event to a `logging.Logger` (default `logging.getLogger("log_foundry")`) so your existing
|
|
327
336
|
handlers, formatters, and `logging.config` apply. Identity fields and the nested `fields` are
|
|
328
337
|
attached to each record; the sink never configures or tears down logging itself.
|
|
329
338
|
|
|
@@ -331,9 +340,9 @@ attached to each record; the sink never configures or tears down logging itself.
|
|
|
331
340
|
|
|
332
341
|
| Sink | Import from | Configure |
|
|
333
342
|
|---|---|---|
|
|
334
|
-
| `FileSink` | `
|
|
335
|
-
| `RotatingFileSink` | `
|
|
336
|
-
| `SQLiteSink` | `
|
|
343
|
+
| `FileSink` | `log_foundry.sinks.file` | `FileSink(path, *, encoding="utf-8")` — append NDJSON to one file |
|
|
344
|
+
| `RotatingFileSink` | `log_foundry.sinks.file` | `RotatingFileSink(path, *, max_bytes=0, backup_count=0, when=None, interval=1)` — rotate by size and/or time, keeping `backup_count` numbered backups |
|
|
345
|
+
| `SQLiteSink` | `log_foundry.sinks.sqlite` | `SQLiteSink(database, *, table="log_events", create_table=True)` — batch-insert into an embedded SQLite DB |
|
|
337
346
|
|
|
338
347
|
`RotatingFileSink`'s time trigger uses a `when` unit code — `"S"`/`"M"`/`"H"`/`"D"` — times `interval`
|
|
339
348
|
(either trigger, or both, can be enabled). `SQLiteSink` stores each event as full JSON plus projected
|
|
@@ -341,7 +350,7 @@ attached to each record; the sink never configures or tears down logging itself.
|
|
|
341
350
|
you provision the table yourself.
|
|
342
351
|
|
|
343
352
|
```python
|
|
344
|
-
from
|
|
353
|
+
from log_foundry.sinks.file import RotatingFileSink
|
|
345
354
|
lf.configure(sink=RotatingFileSink("app.log.jsonl", max_bytes=10_000_000, backup_count=5))
|
|
346
355
|
```
|
|
347
356
|
|
|
@@ -353,15 +362,15 @@ to `HTTPSink` (`headers=`, `auth=`, `gzip=`, `timeout=`, `max_retries=`).
|
|
|
353
362
|
|
|
354
363
|
| Sink | Import from | Configure |
|
|
355
364
|
|---|---|---|
|
|
356
|
-
| `HTTPSink` | `
|
|
357
|
-
| `ElasticsearchSink` | `
|
|
358
|
-
| `OpenSearchSink` | `
|
|
359
|
-
| `LokiSink` | `
|
|
360
|
-
| `LogstashSink` | `
|
|
361
|
-
| `SyslogSink` | `
|
|
365
|
+
| `HTTPSink` | `log_foundry.sinks.http` | `HTTPSink(url, *, method="POST", headers=None, auth=None, body_format="ndjson", timeout=5.0, gzip=False, max_retries=3)` — generic POST. `auth` is a bearer-token `str` or `(user, pass)` for basic; `body_format` is `"ndjson"` or `"json_array"` |
|
|
366
|
+
| `ElasticsearchSink` | `log_foundry.sinks.elasticsearch` | `ElasticsearchSink(url, *, index, auth=None, **http_kwargs)` — POST to `_bulk`, parsing per-item errors (`.item_errors`) |
|
|
367
|
+
| `OpenSearchSink` | `log_foundry.sinks.elasticsearch` | same signature as `ElasticsearchSink` (identical bulk protocol) |
|
|
368
|
+
| `LokiSink` | `log_foundry.sinks.loki` | `LokiSink(url, *, labels=("service", "env", "level"), **http_kwargs)` — Grafana Loki push API |
|
|
369
|
+
| `LogstashSink` | `log_foundry.sinks.logstash` | `LogstashSink(url=…, **http_kwargs)` for HTTP, **or** `LogstashSink(host=…, port=…, transport="tcp")` for a raw TCP/UDP socket |
|
|
370
|
+
| `SyslogSink` | `log_foundry.sinks.syslog` | `SyslogSink(host, port=514, *, transport="udp", facility="user", app_name="log-foundry")` — RFC 5424 over UDP/TCP |
|
|
362
371
|
|
|
363
372
|
```python
|
|
364
|
-
from
|
|
373
|
+
from log_foundry.sinks.elasticsearch import ElasticsearchSink
|
|
365
374
|
lf.configure(sink=ElasticsearchSink("https://es.internal:9200", index="app-logs",
|
|
366
375
|
auth=("elastic", "…")))
|
|
367
376
|
```
|
|
@@ -373,11 +382,11 @@ Also HTTP-based. All are zero-dependency **except** `SentrySink`, which prefers
|
|
|
373
382
|
|
|
374
383
|
| Sink | Import from | Extra | Configure |
|
|
375
384
|
|---|---|---|---|
|
|
376
|
-
| `DatadogSink` | `
|
|
377
|
-
| `SplunkHECSink` | `
|
|
378
|
-
| `NewRelicSink` | `
|
|
379
|
-
| `HoneycombSink` | `
|
|
380
|
-
| `SentrySink` | `
|
|
385
|
+
| `DatadogSink` | `log_foundry.sinks.datadog` | — | `DatadogSink(api_key, *, site="datadoghq.com", service=None, ddtags=None)` |
|
|
386
|
+
| `SplunkHECSink` | `log_foundry.sinks.splunk` | — | `SplunkHECSink(url, token, *, host=None, source="log-foundry")` — HTTP Event Collector |
|
|
387
|
+
| `NewRelicSink` | `log_foundry.sinks.newrelic` | — | `NewRelicSink(api_key, *, region="US")` — `region` is `"US"` or `"EU"` |
|
|
388
|
+
| `HoneycombSink` | `log_foundry.sinks.honeycomb` | — | `HoneycombSink(api_key, dataset, *, url="https://api.honeycomb.io")` |
|
|
389
|
+
| `SentrySink` | `log_foundry.sinks.sentry` | `sentry` | `SentrySink(dsn=None, *, min_level="ERROR")` — sends only `min_level`+ events |
|
|
381
390
|
|
|
382
391
|
With the `sentry` extra installed, `SentrySink` captures via `sentry_sdk.capture_event` (initialize
|
|
383
392
|
the SDK yourself with `sentry_sdk.init(...)`); without it, pass `dsn=` and events are POSTed as
|
|
@@ -386,19 +395,19 @@ Sentry envelopes over HTTP.
|
|
|
386
395
|
#### AWS — the durable-buffer path (`aws` extra)
|
|
387
396
|
|
|
388
397
|
`pip install 'log-foundry[aws]'` (pulls `boto3`). Credentials and region come from boto3's standard
|
|
389
|
-
chain — log-
|
|
398
|
+
chain — log-foundry adds none of its own. Each re-chunks every batch to the service's hard per-request
|
|
390
399
|
limits, retries partial failures, and drops any single event too large to ever fit (counted on
|
|
391
400
|
`.dropped_oversized`).
|
|
392
401
|
|
|
393
402
|
| Sink | Import from | Configure |
|
|
394
403
|
|---|---|---|
|
|
395
|
-
| `SQSSink` | `
|
|
396
|
-
| `SNSSink` | `
|
|
397
|
-
| `KinesisSink` | `
|
|
398
|
-
| `FirehoseSink` | `
|
|
404
|
+
| `SQSSink` | `log_foundry.sinks.sqs` | `SQSSink(queue_url, *, max_retries=3)` — the headline production path: a durable buffer in front of ELK, absorbing downstream spikes/outages |
|
|
405
|
+
| `SNSSink` | `log_foundry.sinks.sns` | `SNSSink(topic_arn, *, max_retries=3)` |
|
|
406
|
+
| `KinesisSink` | `log_foundry.sinks.kinesis` | `KinesisSink(stream_name, *, partition_key_field="trace_id", max_retries=3)` |
|
|
407
|
+
| `FirehoseSink` | `log_foundry.sinks.firehose` | `FirehoseSink(delivery_stream, *, max_retries=3)` |
|
|
399
408
|
|
|
400
409
|
```python
|
|
401
|
-
from
|
|
410
|
+
from log_foundry.sinks.sqs import SQSSink
|
|
402
411
|
lf.configure(service="payments",
|
|
403
412
|
sink=SQSSink(queue_url="https://sqs.us-east-1.amazonaws.com/123456789012/logs"))
|
|
404
413
|
```
|
|
@@ -411,16 +420,16 @@ Each needs its own extra (lazy-imported). All publish + retry within a bound and
|
|
|
411
420
|
|
|
412
421
|
| Sink | Import from | Extra | Configure |
|
|
413
422
|
|---|---|---|---|
|
|
414
|
-
| `KafkaSink` | `
|
|
415
|
-
| `RedisStreamsSink` | `
|
|
416
|
-
| `RedisListSink` | `
|
|
417
|
-
| `RabbitMQSink` | `
|
|
418
|
-
| `NATSSink` | `
|
|
419
|
-
| `GooglePubSubSink` | `
|
|
420
|
-
| `AzureEventHubsSink` | `
|
|
423
|
+
| `KafkaSink` | `log_foundry.sinks.kafka` | `kafka` | `KafkaSink(topic, *, bootstrap_servers="…", key_field="trace_id")` |
|
|
424
|
+
| `RedisStreamsSink` | `log_foundry.sinks.redis` | `redis` | `RedisStreamsSink(stream, *, url=None)` — `XADD` |
|
|
425
|
+
| `RedisListSink` | `log_foundry.sinks.redis` | `redis` | `RedisListSink(key, *, url=None)` — `RPUSH` |
|
|
426
|
+
| `RabbitMQSink` | `log_foundry.sinks.rabbitmq` | `amqp` | `RabbitMQSink(*, exchange, routing_key, url=None)` — persistent messages |
|
|
427
|
+
| `NATSSink` | `log_foundry.sinks.nats` | `nats` | `NATSSink(subject, *, jetstream=False, servers=None)` |
|
|
428
|
+
| `GooglePubSubSink` | `log_foundry.sinks.pubsub` | `gcp-pubsub` | `GooglePubSubSink(topic)` |
|
|
429
|
+
| `AzureEventHubsSink` | `log_foundry.sinks.eventhubs` | `azure-eventhubs` | `AzureEventHubsSink(*, connection_str="…", eventhub=None)` |
|
|
421
430
|
|
|
422
431
|
```python
|
|
423
|
-
from
|
|
432
|
+
from log_foundry.sinks.kafka import KafkaSink
|
|
424
433
|
lf.configure(sink=KafkaSink("app-logs", bootstrap_servers="broker:9092"))
|
|
425
434
|
```
|
|
426
435
|
|
|
@@ -430,9 +439,9 @@ Write-only inserts (querying is the downstream tool's job); each needs its own e
|
|
|
430
439
|
|
|
431
440
|
| Sink | Import from | Extra | Configure |
|
|
432
441
|
|---|---|---|---|
|
|
433
|
-
| `MongoDBSink` | `
|
|
434
|
-
| `PostgresSink` | `
|
|
435
|
-
| `ClickHouseSink` | `
|
|
442
|
+
| `MongoDBSink` | `log_foundry.sinks.mongodb` | `mongo` | `MongoDBSink(*, uri="…", database="…", collection="…")` |
|
|
443
|
+
| `PostgresSink` | `log_foundry.sinks.postgres` | `postgres` | `PostgresSink(table, *, dsn="…", create_table=False)` — JSONB `event` column + extracted columns |
|
|
444
|
+
| `ClickHouseSink` | `log_foundry.sinks.clickhouse` | `clickhouse` | `ClickHouseSink(table, *, dsn="…", create_table=False)` — MergeTree, columnar insert |
|
|
436
445
|
|
|
437
446
|
`PostgresSink` / `ClickHouseSink` default `create_table=False` (you own the schema and indexes); set
|
|
438
447
|
it `True` for an idempotent `CREATE TABLE IF NOT EXISTS` convenience.
|
|
@@ -452,7 +461,7 @@ them (`worker.dropped`) rather than stalling.
|
|
|
452
461
|
Because delivery is asynchronous, drain before the process exits:
|
|
453
462
|
|
|
454
463
|
```python
|
|
455
|
-
import
|
|
464
|
+
import log_foundry as lf
|
|
456
465
|
|
|
457
466
|
lf.shutdown() # flush buffered events and close the sink; blocks until drained
|
|
458
467
|
```
|
|
@@ -497,7 +506,7 @@ poetry run mypy # typecheck (strict, over src/)
|
|
|
497
506
|
every pull request and on push to `main`. A second workflow
|
|
498
507
|
([`spec-lint.yml`](.github/workflows/spec-lint.yml)) lints the design specs under `docs/specs/`.
|
|
499
508
|
|
|
500
|
-
The library uses a src layout (`src/
|
|
509
|
+
The library uses a src layout (`src/log_foundry/`) with a single concept per module: `config`,
|
|
501
510
|
`ids`, `model`, `context`, `decorator`, `api`, `console`, `worker`, and the `sinks/` package (the
|
|
502
511
|
`base` protocol, `stdout`, and one module per sink family — see [Sinks](#sinks)).
|
|
503
512
|
Deeper design docs live in [`docs/`](docs/) — start with [`docs/architecture.md`](docs/architecture.md).
|
|
@@ -513,8 +522,8 @@ sdist and a wheel:
|
|
|
513
522
|
|
|
514
523
|
| Trigger | Version built | Published to PyPI as |
|
|
515
524
|
|---|---|---|
|
|
516
|
-
| merge to `main` | `
|
|
517
|
-
| push tag `
|
|
525
|
+
| merge to `main` | `X.Y.Z.devN` | dev pre-release |
|
|
526
|
+
| push tag `vX.Y.Z` | `X.Y.Z` | stable release |
|
|
518
527
|
|
|
519
528
|
Dev pre-releases keep the upload path exercised on every merge, so a real release is never the
|
|
520
529
|
first time it runs. `pip install log-foundry` still resolves to the latest **stable** version —
|
|
@@ -523,7 +532,7 @@ pip ignores pre-releases unless you pass `--pre`.
|
|
|
523
532
|
Cutting a release is one tag:
|
|
524
533
|
|
|
525
534
|
```bash
|
|
526
|
-
git tag -a v0.2.0 -m "log-
|
|
535
|
+
git tag -a v0.2.0 -m "log-foundry 0.2.0"
|
|
527
536
|
git push origin v0.2.0
|
|
528
537
|
```
|
|
529
538
|
|