log-foundry 0.10.2.dev129__tar.gz → 1.0.1__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.10.2.dev129 → log_foundry-1.0.1}/PKG-INFO +103 -55
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/README.md +101 -54
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/pyproject.toml +7 -4
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/_lifecycle.py +744 -732
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/console.py +1 -1
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_chunk.py +6 -1
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_retry.py +68 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_socket.py +26 -6
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/base.py +7 -2
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/clickhouse.py +35 -7
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/elasticsearch.py +13 -2
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/file.py +49 -29
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/http.py +115 -9
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/logging_sink.py +5 -1
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/logstash.py +44 -10
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/memory.py +5 -2
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/mongodb.py +117 -2
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/nats.py +25 -6
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/postgres.py +22 -8
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/pubsub.py +13 -5
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/rabbitmq.py +53 -1
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/redis.py +16 -3
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sentry.py +30 -4
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/syslog.py +12 -2
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/worker.py +238 -533
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/LICENSE +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/_fork.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/results.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/null.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/transform.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: log-foundry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -41,6 +41,7 @@ Requires-Dist: psycopg[binary] (>=3.1) ; extra == "postgres"
|
|
|
41
41
|
Requires-Dist: pymongo (>=4.6) ; extra == "mongo"
|
|
42
42
|
Requires-Dist: redis (>=5.0) ; extra == "redis"
|
|
43
43
|
Requires-Dist: sentry-sdk (>=2.66.1) ; extra == "sentry"
|
|
44
|
+
Project-URL: Changelog, https://github.com/agriffi10/log-forge/blob/main/CHANGELOG.md
|
|
44
45
|
Project-URL: Documentation, https://github.com/agriffi10/log-forge#readme
|
|
45
46
|
Project-URL: Homepage, https://github.com/agriffi10/log-forge
|
|
46
47
|
Project-URL: Issues, https://github.com/agriffi10/log-forge/issues
|
|
@@ -51,6 +52,11 @@ Description-Content-Type: text/markdown
|
|
|
51
52
|
|
|
52
53
|
# Log Foundry
|
|
53
54
|
|
|
55
|
+
[](https://pypi.org/project/log-foundry/)
|
|
56
|
+
[](https://pypi.org/project/log-foundry/)
|
|
57
|
+
[](https://github.com/agriffi10/log-forge/actions/workflows/release.yml)
|
|
58
|
+
[](https://github.com/agriffi10/log-forge/blob/main/LICENSE)
|
|
59
|
+
|
|
54
60
|
Consistent, structured (JSON) logs for every decorated function call — correlated by shared
|
|
55
61
|
trace/span IDs, ready to ship to any of 30-plus built-in sinks (stdout by default; SQS → ELK is
|
|
56
62
|
the headline production path).
|
|
@@ -71,6 +77,27 @@ calls form a tree you can query later.
|
|
|
71
77
|
a level call with **no open span**, which emits on your own thread, and `flush()`, which by
|
|
72
78
|
definition waits for the drain it asked for.
|
|
73
79
|
|
|
80
|
+
**The public API is frozen for the whole of `1.x`.** Everything in `log_foundry.__all__`, the
|
|
81
|
+
`Sink` protocol, and every shipped sink class **at the import path documented for it below** —
|
|
82
|
+
together with the public names those signatures use, such as `GroupIdSource`, `DedupIdSource` and
|
|
83
|
+
`Backend` — stays put: nothing is removed, renamed or moved, and no signature changes in a way
|
|
84
|
+
that breaks a caller, until `2.0.0`. The import path is part of that promise because it has to
|
|
85
|
+
be — no concrete sink is exported from the top level, so `from log_foundry.sinks.sqs import
|
|
86
|
+
SQSSink` is the only way to reach one, and a class pinned at a path free to move is not pinned at
|
|
87
|
+
all. `1.0.0` is where they last moved; the upgrade note below says which.
|
|
88
|
+
|
|
89
|
+
Behaviour is not frozen by that promise — a defect is still a defect, and fixing one can change
|
|
90
|
+
what a broken path does. The private half is genuinely private: underscore-prefixed modules —
|
|
91
|
+
`_lifecycle`, `_fork`, `_diag`, and the `sinks/_*.py` helpers — move without notice.
|
|
92
|
+
|
|
93
|
+
`1.0.0` was a reliability release rather than a feature release: three audit arcs worked through
|
|
94
|
+
the paths where the library could lose an event, fail its caller, block forever, or report health
|
|
95
|
+
it could not back up. If you are coming from any `0.x`, read its release notes before you bump —
|
|
96
|
+
several of the changes are silent, and they are written against `0.10.1`, the release immediately
|
|
97
|
+
before this one:
|
|
98
|
+
[`docs/release-notes/v1.0.0.md`](https://github.com/agriffi10/log-forge/blob/v1.0.0/docs/release-notes/v1.0.0.md).
|
|
99
|
+
[`CHANGELOG.md`](https://github.com/agriffi10/log-forge/blob/main/CHANGELOG.md) indexes every version.
|
|
100
|
+
|
|
74
101
|
---
|
|
75
102
|
|
|
76
103
|
## Requirements
|
|
@@ -82,12 +109,14 @@ calls form a tree you can query later.
|
|
|
82
109
|
Published on PyPI as **[`log-foundry`](https://pypi.org/project/log-foundry/)**:
|
|
83
110
|
|
|
84
111
|
```bash
|
|
85
|
-
pip install log-foundry
|
|
86
|
-
pip install 'log-foundry[aws]'
|
|
112
|
+
pip install log-foundry # core, zero dependencies
|
|
113
|
+
pip install 'log-foundry[aws]' # + boto3 for the SQS/SNS/Kinesis/Firehose sinks
|
|
114
|
+
pip install 'log-foundry>=1,<2' # pin to the frozen API above
|
|
87
115
|
```
|
|
88
116
|
|
|
89
|
-
> **Breaking in `1.0.0`.** Three public shapes
|
|
90
|
-
>
|
|
117
|
+
> **Breaking in `1.0.0`, if you are upgrading from any `0.x`.** Three public shapes changed once,
|
|
118
|
+
> in the release that froze the API, because none of them could have been changed afterwards
|
|
119
|
+
> without a major version:
|
|
91
120
|
>
|
|
92
121
|
> - **`health()` and `sink.losses()` return frozen dataclasses**, not `NamedTuple`s. Attribute
|
|
93
122
|
> access (`h.dropped`, `losses.failed`) is unchanged and is the whole contract; `len(h)`,
|
|
@@ -97,29 +126,36 @@ pip install 'log-foundry[aws]' # + boto3 for the SQS/SNS/Kinesis/Firehose sink
|
|
|
97
126
|
> - **`flush()` returns a `FlushResult` and `continue_trace()` a `ContinueResult`**, each truthy
|
|
98
127
|
> or falsy with a `reason` naming *why*. `if lf.flush():` is unchanged; **`lf.flush() is True`
|
|
99
128
|
> is not** — the result is an object. A one-bit return could not grow a reason later without
|
|
100
|
-
> silently changing what `if flush():` means, which is why it moved
|
|
129
|
+
> silently changing what `if flush():` means, which is why it moved before the freeze.
|
|
101
130
|
> - **`SQSSink`'s injected client is keyword-only** (`SQSSink(queue_url, client=…)`), **`SentrySink`
|
|
102
131
|
> injects through `client=`** rather than the old `sdk` keyword, with no alias, and the sink attribute the
|
|
103
132
|
> library assigns for interruptible backoff is **`log_foundry_stop_signal`**, not
|
|
104
133
|
> `stop_signal` — a prefixed name cannot silently overwrite one your own sink already uses.
|
|
105
134
|
>
|
|
135
|
+
> Those are the three changes of *shape*, and not the three most likely to reach you:
|
|
136
|
+
> `log_foundry.sinks.util` was deleted with no alias — the `MemorySink`, `NullSink` and
|
|
137
|
+
> `StderrSink` it held are now at `log_foundry.sinks.memory`, `…null` and `…stdout` — and
|
|
138
|
+
> several new construction-time refusals land, all of which rank above two of the three above in
|
|
139
|
+
> the release notes' own ordering. Read the full list of twenty-two before you upgrade —
|
|
140
|
+
> [`docs/release-notes/v1.0.0.md`](https://github.com/agriffi10/log-forge/blob/v1.0.0/docs/release-notes/v1.0.0.md).
|
|
141
|
+
>
|
|
106
142
|
> `echo`, `message` and `fields` are reserved parameter names on the emitters; pass fields of
|
|
107
143
|
> those names through `fields={...}`, which also takes keys that are not Python identifiers.
|
|
108
144
|
|
|
109
|
-
> **
|
|
110
|
-
>
|
|
111
|
-
>
|
|
112
|
-
>
|
|
113
|
-
>
|
|
114
|
-
>
|
|
115
|
-
>
|
|
116
|
-
> `log_foundry`.
|
|
145
|
+
> **Why `log-foundry`, when the repository is `log-forge`.** The distribution and the import
|
|
146
|
+
> package match each other — `pip install log-foundry`, then `import log_foundry`. The project was
|
|
147
|
+
> originally called *log-forge*, but PyPI rejects that name as too similar to the unrelated,
|
|
148
|
+
> pre-existing [`logforge`](https://pypi.org/project/logforge/) project — its similarity check
|
|
149
|
+
> collapses separators, so `log-forge` and `logforge` count as the same name. Rather than keep a
|
|
150
|
+
> distribution and an import name that disagreed, everything became `log-foundry` / `log_foundry`
|
|
151
|
+
> in `0.2.0`.
|
|
117
152
|
>
|
|
118
|
-
>
|
|
119
|
-
> and no public API changed. A handful of *emitted*
|
|
120
|
-
> `LoggingSink`'s default logger
|
|
121
|
-
> `
|
|
122
|
-
>
|
|
153
|
+
> If you are still on `0.1.x`, there is no compatibility shim: migrating is a find-and-replace on
|
|
154
|
+
> `log_forge` → `log_foundry`; no module moved and no public API changed. A handful of *emitted*
|
|
155
|
+
> defaults carry the name and shift with it: `LoggingSink`'s default logger
|
|
156
|
+
> (`logging.getLogger("log_foundry")`), `SyslogSink(app_name=…)`, `SplunkHECSink(source=…)`,
|
|
157
|
+
> Datadog's `ddsource`, and Sentry's client tag. Override them explicitly if a downstream query
|
|
158
|
+
> or dashboard pins the old string.
|
|
123
159
|
|
|
124
160
|
```python
|
|
125
161
|
import log_foundry
|
|
@@ -191,7 +227,7 @@ with the child span pointing at its parent via `parent_span_id`:
|
|
|
191
227
|
|
|
192
228
|
## How it works
|
|
193
229
|
|
|
194
|
-

|
|
230
|
+

|
|
195
231
|
|
|
196
232
|
A traced call travels through a small pipeline. The first four steps run on your own thread
|
|
197
233
|
and are deliberately fast; the last two run on a background thread so your code never waits on
|
|
@@ -578,7 +614,8 @@ nothing.
|
|
|
578
614
|
Wire one up by passing an instance to `configure(sink=...)`; if you never do, the first decorated
|
|
579
615
|
call falls back to `StdoutSink()`. The **protocol** is a top-level export, alongside
|
|
580
616
|
`SinkDeliveryError`, `SinkLosses`, `read_losses` and `flush_sink` — the last two being the probes
|
|
581
|
-
a wrapper sink needs to ask a child for its losses and to push its client-side buffer
|
|
617
|
+
a wrapper sink needs to ask a child for its losses and to push its client-side buffer — plus the
|
|
618
|
+
two default bounds, `DEFAULT_SHUTDOWN_TIMEOUT` and `DEFAULT_SWAP_TIMEOUT`. The
|
|
582
619
|
**concrete sinks** are not exported, so import each from its own module, e.g.
|
|
583
620
|
`from log_foundry.sinks.sqs import SQSSink`.
|
|
584
621
|
|
|
@@ -892,8 +929,8 @@ disconnected, so a sustained outage moves `health().failed_batches` instead of b
|
|
|
892
929
|
| `KafkaSink` | `log_foundry.sinks.kafka` | `kafka` | `KafkaSink(topic, *, flush_timeout=10.0, bootstrap_servers="…", key_field="trace_id", producer_config=None)` — `producer_config` is merged **beneath** the sink's own keys, so it reaches `message.timeout.ms` and friends without displacing `bootstrap.servers`; passing it with `producer=` is a `ValueError` |
|
|
893
930
|
| `RedisStreamsSink` | `log_foundry.sinks.redis` | `redis` | `RedisStreamsSink(stream, *, url=None, maxlen=None)` — `XADD`. `maxlen` caps the stream (`approximate=True`); trimming happens **at Redis**, after delivery, so it is invisible to `health()` — which is why the default is unbounded |
|
|
894
931
|
| `RedisListSink` | `log_foundry.sinks.redis` | `redis` | `RedisListSink(key, *, url=None, maxlen=None)` — `RPUSH` + `LTRIM` to the newest `maxlen`; same destination-side trimming caveat |
|
|
895
|
-
| `RabbitMQSink` | `log_foundry.sinks.rabbitmq` | `amqp` | `RabbitMQSink(*, exchange, routing_key, url=None)` — persistent messages |
|
|
896
|
-
| `NATSSink` | `log_foundry.sinks.nats` | `nats` | `NATSSink(subject, *, jetstream=False, servers=None, publish_timeout=10.0, connect_timeout=None, max_reconnect_attempts=None, reconnect_time_wait=None, drain_timeout=None)` — `publish_timeout` bounds one whole `emit` and applies to an injected `client=` too; the four `None` timeouts are forwarded to `nats.connect` only when set, and passing one with `client=` is a `ValueError` |
|
|
932
|
+
| `RabbitMQSink` | `log_foundry.sinks.rabbitmq` | `amqp` | `RabbitMQSink(*, exchange, routing_key, url=None, blocked_connection_timeout=None, socket_timeout=None, stack_timeout=None)` — persistent messages; `pika` leaves `blocked_connection_timeout` unset, so a broker under a memory or disk alarm blocks every publish indefinitely — the sink applies `DEFAULT_BLOCKED_CONNECTION_TIMEOUT` (30 s) unless the URL's query names one, and an explicit keyword overrides the URL. **Read from `pika` 1.4.4 and not executed against a broker** — verify it against yours |
|
|
933
|
+
| `NATSSink` | `log_foundry.sinks.nats` | `nats` | `NATSSink(subject, *, jetstream=False, servers=None, publish_timeout=10.0, connect_timeout=None, max_reconnect_attempts=None, reconnect_time_wait=None, drain_timeout=None)` — a non-positive `max_reconnect_attempts` is a `ValueError` at construction, since `nats-py` retires a server from its pool only under `max_reconnect_attempts > 0` and a non-positive value therefore never returns; `publish_timeout` bounds one whole `emit` and applies to an injected `client=` too; the four `None` timeouts are forwarded to `nats.connect` only when set, and passing one with `client=` is a `ValueError` |
|
|
897
934
|
| `GooglePubSubSink` | `log_foundry.sinks.pubsub` | `gcp-pubsub` | `GooglePubSubSink(topic)` |
|
|
898
935
|
| `AzureEventHubsSink` | `log_foundry.sinks.eventhubs` | `azure-eventhubs` | `AzureEventHubsSink(*, connection_str="…", eventhub=None)` |
|
|
899
936
|
|
|
@@ -908,9 +945,9 @@ Write-only inserts (querying is the downstream tool's job); each needs its own e
|
|
|
908
945
|
|
|
909
946
|
| Sink | Import from | Extra | Configure |
|
|
910
947
|
|---|---|---|---|
|
|
911
|
-
| `MongoDBSink` | `log_foundry.sinks.mongodb` | `mongo` | `MongoDBSink(*, uri="…", database="…", collection="…")` |
|
|
948
|
+
| `MongoDBSink` | `log_foundry.sinks.mongodb` | `mongo` | `MongoDBSink(*, uri="…", database="…", collection="…", socket_timeout=None, server_selection_timeout=None)` — `pymongo`'s `socketTimeoutMS` default is `None`, so a read that never returns holds the drain thread; the sink applies `DEFAULT_SOCKET_TIMEOUT` (30 s) only when neither the keyword nor the URI query names one |
|
|
912
949
|
| `PostgresSink` | `log_foundry.sinks.postgres` | `postgres` | `PostgresSink(table, *, dsn="…", create_table=False, connect_timeout=5)` — JSONB `event` column + extracted columns. Reconnects an **owned** connection the server has closed; a `connection=` you inject is never reopened. `connect_timeout` is passed to libpq explicitly, so it **overrides** any `connect_timeout` in your DSN |
|
|
913
|
-
| `ClickHouseSink` | `log_foundry.sinks.clickhouse` | `clickhouse` | `ClickHouseSink(table, *, dsn="…", create_table=False)` — MergeTree, columnar insert |
|
|
950
|
+
| `ClickHouseSink` | `log_foundry.sinks.clickhouse` | `clickhouse` | `ClickHouseSink(table, *, dsn="…", create_table=False, chunk_size=1000, send_receive_timeout=None)` — MergeTree, columnar insert; the driver's own 300 s `send_receive_timeout` is finite so it is left alone, and forwarded only when you set it |
|
|
914
951
|
|
|
915
952
|
`PostgresSink` / `ClickHouseSink` default `create_table=False` (you own the schema and indexes); set
|
|
916
953
|
it `True` for an idempotent `CREATE TABLE IF NOT EXISTS` convenience.
|
|
@@ -1103,9 +1140,11 @@ reported. They aggregate different failure populations — one can mean the dest
|
|
|
1103
1140
|
data; the other never the destination — the data, or no drain thread at all (SPEC-050) — so a
|
|
1104
1141
|
single number would hide which fix applies.
|
|
1105
1142
|
|
|
1106
|
-
`h.sink` is a `SinkLosses`, carrying `dropped` and `failed`, or `None` — `None` when no
|
|
1107
|
-
|
|
1108
|
-
`losses()` raises reports `None` too).
|
|
1143
|
+
`h.sink` is a `SinkLosses`, carrying `dropped` and `failed`, or `None` — `None` when no sink has
|
|
1144
|
+
been configured at all, and `None` when the configured sink reports nothing (`losses()` is
|
|
1145
|
+
optional, and a sink whose `losses()` raises reports `None` too). It is **not** `None` merely
|
|
1146
|
+
because no worker exists: since SPEC-054 it is answered
|
|
1147
|
+
from the configuration, so it reports on the orphan path too. Note the two `dropped` fields count
|
|
1109
1148
|
different things: the worker's is backpressure at *its* queue, the sink's is an event that never
|
|
1110
1149
|
reached the wire. They are separate because the remedies do not overlap — and `sink.dropped` is
|
|
1111
1150
|
itself two causes, which is why the diagnostic line matters. Most sinks drop only what can never
|
|
@@ -1133,10 +1172,13 @@ logging is doing the right thing; it is the *pair* — retired, and still being
|
|
|
1133
1172
|
means every log line since the shutdown has gone nowhere. That state used to read as perfectly
|
|
1134
1173
|
healthy: `stopped_reason` is `None` after a clean shutdown, and the queue simply grows.
|
|
1135
1174
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1175
|
+
**Six of the twelve fields report for a process that has no worker at all** — `sink`, `retired`,
|
|
1176
|
+
`closing_sinks`, `inherited_sink`, `orphan_lost` and `in_span_lost`. That set is not a list to
|
|
1177
|
+
maintain by hand: it is exactly the fields in `_lifecycle`'s `Health(...)` assembly that are *not*
|
|
1178
|
+
guarded by `counters is None`, and re-reading that call is how to check it. A program that only
|
|
1179
|
+
ever calls `info()`/`error()` outside a span emits synchronously and builds no background worker,
|
|
1180
|
+
so the other six describe something that does not exist and read their empty value — zero for the
|
|
1181
|
+
five counters, `None` for `stopped_reason` — which is why that path needs counters of its own. Until it had them, such a process
|
|
1140
1182
|
reported `queued=0 dropped=0 failed_batches=0 stopped_reason=None` over total, permanent loss, and
|
|
1141
1183
|
the only thing that said otherwise was a line on stderr. Its
|
|
1142
1184
|
`shutdown()` still closes the sink, exactly once and without starting a thread, and `retired` reads
|
|
@@ -1359,7 +1401,7 @@ there. Any ceiling firing sets `truncated: true` on the event.
|
|
|
1359
1401
|
length** (sign included) for an integer. They coincide for ASCII digits, and one ceiling for "how
|
|
1360
1402
|
big may a single value get" was preferred to a second config key. Note that all four ceilings
|
|
1361
1403
|
bound each *value* — an event of many bounded values can still be large; see
|
|
1362
|
-
[Known constraints](docs/architecture.md#known-constraints).
|
|
1404
|
+
[Known constraints](https://github.com/agriffi10/log-forge/blob/main/docs/architecture.md#known-constraints).
|
|
1363
1405
|
|
|
1364
1406
|
## Development
|
|
1365
1407
|
|
|
@@ -1385,7 +1427,7 @@ The library uses a src layout (`src/log_foundry/`) with a single concept per mod
|
|
|
1385
1427
|
the internal `_lifecycle`, `_fork` and `_diag`, and the `sinks/` package (the `base` protocol,
|
|
1386
1428
|
`stdout`, and one module per sink family — see [Sinks](#sinks)). Anything underscore-prefixed
|
|
1387
1429
|
is internal and moves without notice.
|
|
1388
|
-
Deeper design docs live in [`docs/`](docs/) — start with [`docs/architecture.md`](docs/architecture.md).
|
|
1430
|
+
Deeper design docs live in [`docs/`](https://github.com/agriffi10/log-forge/tree/main/docs/) — start with [`docs/architecture.md`](https://github.com/agriffi10/log-forge/blob/main/docs/architecture.md).
|
|
1389
1431
|
|
|
1390
1432
|
### Continuous integration
|
|
1391
1433
|
|
|
@@ -1396,14 +1438,14 @@ worth reading before assuming a PR was audited:
|
|
|
1396
1438
|
|
|
1397
1439
|
| Check | Does | When | Fails the build |
|
|
1398
1440
|
|---|---|---|---|
|
|
1399
|
-
| [`ci.yml`](.github/workflows/ci.yml) | ruff → mypy → pytest, on 3.12 **and** 3.13 | every PR | yes |
|
|
1400
|
-
| [`spec-lint.yml`](.github/workflows/spec-lint.yml) | lints the design specs under `docs/specs/` | specs touched | yes |
|
|
1401
|
-
| [`dependency-review.yml`](.github/workflows/dependency-review.yml) | fails a PR that *introduces* a dependency with a known advisory (`moderate`+) | every PR | yes |
|
|
1402
|
-
| [`zizmor.yml`](.github/workflows/zizmor.yml) | static analysis of the workflow files themselves | workflow, action, dependabot or zizmor config touched; also weekly | no — reports to code scanning |
|
|
1441
|
+
| [`ci.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/ci.yml) | ruff → mypy → pytest, on 3.12 **and** 3.13 | every PR | yes |
|
|
1442
|
+
| [`spec-lint.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/spec-lint.yml) | lints the design specs under `docs/specs/` | specs touched | yes |
|
|
1443
|
+
| [`dependency-review.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/dependency-review.yml) | fails a PR that *introduces* a dependency with a known advisory (`moderate`+) | every PR | yes |
|
|
1444
|
+
| [`zizmor.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/zizmor.yml) | static analysis of the workflow files themselves | workflow, action, dependabot or zizmor config touched; also weekly | no — reports to code scanning |
|
|
1403
1445
|
| CodeQL | `python` + `actions`, `extended` query suite; also weekly | every PR | no — reports to code scanning |
|
|
1404
|
-
| [`integration.yml`](.github/workflows/integration.yml) | the extras-backed sinks against nine real services in containers | sinks, `tests/integration/`, `pyproject.toml`, `poetry.lock` or that workflow touched; also weekly | it goes red, and like every check here it is advisory — `main` requires no status check at all, and this one is furthest from earning that: nine service containers are a flakiness budget |
|
|
1405
|
-
| [`pip-audit.yml`](.github/workflows/pip-audit.yml) | advisories across every extra, `--strict` | **not on PRs at all** — on the merge to `main` when the lockfile, extras, the ignore list or that workflow moved, and weekly | yes, on `main` |
|
|
1406
|
-
| [`scorecard.yml`](.github/workflows/scorecard.yml) | OpenSSF Scorecard over the repository's own supply chain | **not on PRs** — on the merge to `main` when `.github/`, `scripts/`, `SECURITY.md`, `LICENSE` or the lockfile moved, and weekly | no — reports to code scanning |
|
|
1446
|
+
| [`integration.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/integration.yml) | the extras-backed sinks against nine real services in containers | sinks, `tests/integration/`, `pyproject.toml`, `poetry.lock` or that workflow touched; also weekly | it goes red, and like every check here it is advisory — `main` requires no status check at all, and this one is furthest from earning that: nine service containers are a flakiness budget |
|
|
1447
|
+
| [`pip-audit.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/pip-audit.yml) | advisories across every extra, `--strict` | **not on PRs at all** — on the merge to `main` when the lockfile, extras, the ignore list or that workflow moved, and weekly | yes, on `main` |
|
|
1448
|
+
| [`scorecard.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/scorecard.yml) | OpenSSF Scorecard over the repository's own supply chain | **not on PRs** — on the merge to `main` when `.github/`, `scripts/`, `SECURITY.md`, `LICENSE` or the lockfile moved, and weekly | no — reports to code scanning |
|
|
1407
1449
|
|
|
1408
1450
|
**`scripts/docs-lint.sh` is deliberately not in that table**, because nothing in CI runs it. It
|
|
1409
1451
|
holds the always-loaded documentation tier to its budgets and is a local pre-push gate, so its
|
|
@@ -1411,8 +1453,8 @@ failure lands on whoever caused it rather than on a shared branch. Contributors
|
|
|
1411
1453
|
with `scripts/spec-lint.sh`, before pushing.
|
|
1412
1454
|
|
|
1413
1455
|
On a push to `main` the full `ci.yml` matrix still runs, but as the first job of
|
|
1414
|
-
[`release.yml`](.github/workflows/release.yml), which `uses:` this same workflow before it
|
|
1415
|
-
allowed to publish — so on main the checks are listed under the *Release* workflow, named
|
|
1456
|
+
[`release.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/release.yml), which `uses:` this same workflow before it would
|
|
1457
|
+
be allowed to publish — so on main the checks are listed under the *Release* workflow, named
|
|
1416
1458
|
`test / test (py3.12)` and `test / test (py3.13)`. `ci.yml` deliberately carries no `push`
|
|
1417
1459
|
trigger of its own; it had one, and the result was that every merge ran the identical matrix
|
|
1418
1460
|
twice. A `v*` tag is gated the same way, by that same reusable call.
|
|
@@ -1422,7 +1464,7 @@ why there is no `codeql.yml` here (adding one would disable the default setup an
|
|
|
1422
1464
|
the uploads). The two scanners that don't fail a build report findings to code scanning
|
|
1423
1465
|
deliberately: the alert count is the verdict there, not the green check mark.
|
|
1424
1466
|
|
|
1425
|
-
[`dependabot.yml`](.github/dependabot.yml) opens scheduled version updates for `pip` and
|
|
1467
|
+
[`dependabot.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/dependabot.yml) opens scheduled version updates for `pip` and
|
|
1426
1468
|
`github-actions` on top of the security updates GitHub raises against advisories. Both ecosystems
|
|
1427
1469
|
use a cooldown so a freshly published release isn't adopted within hours of appearing, and `pip`
|
|
1428
1470
|
uses `increase-if-necessary` so an update never narrows a floor this library publishes to its
|
|
@@ -1432,7 +1474,7 @@ consumers.
|
|
|
1432
1474
|
|
|
1433
1475
|
Please report a vulnerability through GitHub's **private** reporting rather than a public issue:
|
|
1434
1476
|
[**open a draft advisory**](https://github.com/agriffi10/log-forge/security/advisories/new).
|
|
1435
|
-
[`SECURITY.md`](SECURITY.md) covers what to include and what to expect — an acknowledgement
|
|
1477
|
+
[`SECURITY.md`](https://github.com/agriffi10/log-forge/blob/main/SECURITY.md) covers what to include and what to expect — an acknowledgement
|
|
1436
1478
|
within 7 days, an assessment within 30. Fixes land on the latest released minor; there are no
|
|
1437
1479
|
long-term support branches.
|
|
1438
1480
|
|
|
@@ -1448,7 +1490,7 @@ everything it instruments:
|
|
|
1448
1490
|
- **Every release ships a CycloneDX SBOM** as a release asset
|
|
1449
1491
|
([latest release](https://github.com/agriffi10/log-forge/releases/latest),
|
|
1450
1492
|
`log-foundry-X.Y.Z.cdx.json`), describing the published wheel with every extra installed.
|
|
1451
|
-
[`SECURITY.md`](SECURITY.md#software-bill-of-materials) has the detail.
|
|
1493
|
+
[`SECURITY.md`](https://github.com/agriffi10/log-forge/blob/main/SECURITY.md#software-bill-of-materials) has the detail.
|
|
1452
1494
|
|
|
1453
1495
|
Scanning runs continuously rather than at release time: CodeQL over the source and the workflows,
|
|
1454
1496
|
zizmor over the workflows, `dependency-review` on every pull request, `pip-audit` across all
|
|
@@ -1464,17 +1506,20 @@ is published on the advisory database's clock, not on this repository's.
|
|
|
1464
1506
|
`poetry-dynamic-versioning`, so `pyproject.toml` carries no literal version and the published
|
|
1465
1507
|
number can't drift from what Git says.
|
|
1466
1508
|
|
|
1467
|
-
[`release.yml`](.github/workflows/release.yml) reuses the CI suite as a gate, then builds an
|
|
1509
|
+
[`release.yml`](https://github.com/agriffi10/log-forge/blob/main/.github/workflows/release.yml) reuses the CI suite as a gate, then builds an
|
|
1468
1510
|
sdist and a wheel:
|
|
1469
1511
|
|
|
1470
1512
|
| Trigger | Version built | Published to PyPI as |
|
|
1471
1513
|
|---|---|---|
|
|
1472
|
-
| merge to `main` | `X.Y.Z.devN` |
|
|
1514
|
+
| merge to `main` | `X.Y.Z.devN` | **nothing, for now** — `publish-dev` is disabled |
|
|
1473
1515
|
| push tag `vX.Y.Z` | `X.Y.Z` | stable release |
|
|
1474
1516
|
|
|
1475
|
-
Dev pre-releases
|
|
1476
|
-
first time it
|
|
1477
|
-
|
|
1517
|
+
Dev pre-releases **kept** the upload path exercised on every merge, so a real release was never
|
|
1518
|
+
the first time it ran. That property is suspended along with the job, so **every** tag is now the
|
|
1519
|
+
first upload attempted since the previous one — `v1.0.0` was the first to sit in that position
|
|
1520
|
+
after `publish-dev` was disabled, and it succeeded, but nothing between releases exercises the
|
|
1521
|
+
path any more, so the next tag is always exposed the same way. `pip install log-foundry` resolves
|
|
1522
|
+
to the latest **stable** version either way — pip ignores pre-releases unless you pass `--pre`.
|
|
1478
1523
|
|
|
1479
1524
|
Cutting a release is one tag:
|
|
1480
1525
|
|
|
@@ -1489,9 +1534,12 @@ one from the commit range. Both are valid, but the lookup is by exact tag name a
|
|
|
1489
1534
|
takes the tag, so notes written for one version do nothing for another and notes added after
|
|
1490
1535
|
the tagged commit are not seen. A release body cannot be amended once published — this
|
|
1491
1536
|
repository has immutable releases — so check the file is there and named for the tag before
|
|
1492
|
-
pushing it. [`
|
|
1493
|
-
|
|
1494
|
-
|
|
1537
|
+
pushing it. **Add the version's section to [`CHANGELOG.md`](https://github.com/agriffi10/log-forge/blob/main/CHANGELOG.md) in the same commit** —
|
|
1538
|
+
it is user-facing and no gate holds it to the tags, so it goes stale by being forgotten.
|
|
1539
|
+
[`docs/release-notes/`](https://github.com/agriffi10/log-forge/tree/main/docs/release-notes/) holds the notes written so far, and
|
|
1540
|
+
[`docs/spec-delivery/RELEASES.md`](https://github.com/agriffi10/log-forge/blob/main/docs/spec-delivery/RELEASES.md) records which specs each
|
|
1541
|
+
version carried — its top row is written in the commit that version's tag is cut from, so it can
|
|
1542
|
+
name a tag that does not exist yet.
|
|
1495
1543
|
|
|
1496
1544
|
Uploads authenticate with PyPI [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
|
|
1497
1545
|
(OIDC) through the `pypi` GitHub Environment — there is no API token stored in the repository.
|
|
@@ -1505,5 +1553,5 @@ deliberately off the rolling `release/v1` branch PyPA recommends, because this i
|
|
|
1505
1553
|
|
|
1506
1554
|
## License
|
|
1507
1555
|
|
|
1508
|
-
[MIT](LICENSE) © Andrew Griffith
|
|
1556
|
+
[MIT](https://github.com/agriffi10/log-forge/blob/main/LICENSE) © Andrew Griffith
|
|
1509
1557
|
|