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.
Files changed (59) hide show
  1. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/PKG-INFO +103 -55
  2. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/README.md +101 -54
  3. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/pyproject.toml +7 -4
  4. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/_lifecycle.py +744 -732
  5. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/console.py +1 -1
  6. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_chunk.py +6 -1
  7. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_retry.py +68 -0
  8. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_socket.py +26 -6
  9. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/base.py +7 -2
  10. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/clickhouse.py +35 -7
  11. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/elasticsearch.py +13 -2
  12. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/file.py +49 -29
  13. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/http.py +115 -9
  14. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/logging_sink.py +5 -1
  15. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/logstash.py +44 -10
  16. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/memory.py +5 -2
  17. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/mongodb.py +117 -2
  18. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/nats.py +25 -6
  19. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/postgres.py +22 -8
  20. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/pubsub.py +13 -5
  21. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/rabbitmq.py +53 -1
  22. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/redis.py +16 -3
  23. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sentry.py +30 -4
  24. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/syslog.py +12 -2
  25. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/worker.py +238 -533
  26. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/LICENSE +0 -0
  27. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/__init__.py +0 -0
  28. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/_diag.py +0 -0
  29. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/_fork.py +0 -0
  30. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/api.py +0 -0
  31. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/config.py +0 -0
  32. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/context.py +0 -0
  33. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/decorator.py +0 -0
  34. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/ids.py +0 -0
  35. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/model.py +0 -0
  36. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/py.typed +0 -0
  37. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/results.py +0 -0
  38. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sanitize.py +0 -0
  39. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/__init__.py +0 -0
  40. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_batch.py +0 -0
  41. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/_time.py +0 -0
  42. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/callback.py +0 -0
  43. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/datadog.py +0 -0
  44. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/eventhubs.py +0 -0
  45. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/filtering.py +0 -0
  46. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/firehose.py +0 -0
  47. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/honeycomb.py +0 -0
  48. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/kafka.py +0 -0
  49. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/kinesis.py +0 -0
  50. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/loki.py +0 -0
  51. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/multi.py +0 -0
  52. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/newrelic.py +0 -0
  53. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/null.py +0 -0
  54. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sns.py +0 -0
  55. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/splunk.py +0 -0
  56. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sqlite.py +0 -0
  57. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/sqs.py +0 -0
  58. {log_foundry-0.10.2.dev129 → log_foundry-1.0.1}/src/log_foundry/sinks/stdout.py +0 -0
  59. {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.10.2.dev129
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
+ [![PyPI](https://img.shields.io/pypi/v/log-foundry?label=pypi)](https://pypi.org/project/log-foundry/)
56
+ [![Python](https://img.shields.io/pypi/pyversions/log-foundry)](https://pypi.org/project/log-foundry/)
57
+ [![Build](https://img.shields.io/github/actions/workflow/status/agriffi10/log-forge/release.yml?branch=main&label=build)](https://github.com/agriffi10/log-forge/actions/workflows/release.yml)
58
+ [![License](https://img.shields.io/pypi/l/log-foundry)](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 # core, zero dependencies
86
- pip install 'log-foundry[aws]' # + boto3 for the SQS/SNS/Kinesis/Firehose sinks
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 change once, before the API is frozen under
90
- > semantic versioning, because none of them could be changed afterwards without a major version:
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 now.
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
- > **Renamed in 0.2.0: `log_forge` → `log_foundry`.** The import package now matches the
110
- > distribution name — `pip install log-foundry`, then `import log_foundry`. If you are on
111
- > `0.1.x`, update your imports; there is no compatibility shim. The project was originally
112
- > called *log-forge*, but PyPI rejects that name as too similar to the unrelated, pre-existing
113
- > [`logforge`](https://pypi.org/project/logforge/) project — its similarity check collapses
114
- > separators, so `log-forge` and `logforge` count as the same name. Rather than keep a
115
- > distribution and an import name that disagreed, everything is now `log-foundry` /
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
- > Migrating from `0.1.x` is a find-and-replace on `log_forge` → `log_foundry`; no module moved
119
- > and no public API changed. A handful of *emitted* defaults carry the name and shift with it:
120
- > `LoggingSink`'s default logger (`logging.getLogger("log_foundry")`), `SyslogSink(app_name=…)`,
121
- > `SplunkHECSink(source=…)`, Datadog's `ddsource`, and Sentry's client tag. Override them
122
- > explicitly if a downstream query or dashboard pins the old string.
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
- ![log-foundry pipeline: a traced call opens a span, gathers events, then closes and hands off to a background worker that batches events and ships them to a sink. Steps 1–4 run on your thread; the worker and sink run on a background thread. Support modules — config, ids, model, context, console — assist every step.](docs/assets/pipeline.svg)
230
+ ![log-foundry pipeline: a traced call opens a span, gathers events, then closes and hands off to a background worker that batches events and ships them to a sink. Steps 1–4 run on your thread; the worker and sink run on a background thread. Support modules — config, ids, model, context, console — assist every step.](https://raw.githubusercontent.com/agriffi10/log-forge/main/docs/assets/pipeline.svg)
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. The
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 worker
1107
- exists yet, or when the configured sink reports nothing (`losses()` is optional, and a sink whose
1108
- `losses()` raises reports `None` too). Note the two `dropped` fields count
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
- `retired`, `orphan_lost` and `in_span_lost` are the fields reported for a process that has **no
1137
- worker at all**. A program that only ever calls `info()`/`error()` outside a span emits
1138
- synchronously and builds no background worker, so the rest describe something that does not exist
1139
- and read zero — which is why that path needs counters of its own. Until it had them, such a process
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 is
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` | dev pre-release |
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 keep the upload path exercised on every merge, so a real release is never the
1476
- first time it runs. `pip install log-foundry` still resolves to the latest **stable** version —
1477
- pip ignores pre-releases unless you pass `--pre`.
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. [`docs/release-notes/`](docs/release-notes/) holds the ones written so far, and
1493
- [`docs/spec-delivery/RELEASES.md`](docs/spec-delivery/RELEASES.md) records which specs each
1494
- released version carried.
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