log-foundry 0.10.2.dev44__tar.gz → 0.10.2.dev45__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.dev44 → log_foundry-0.10.2.dev45}/PKG-INFO +53 -14
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/README.md +52 -13
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/__init__.py +10 -5
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/decorator.py +36 -8
- log_foundry-0.10.2.dev45/src/log_foundry/results.py +65 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/base.py +4 -2
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/worker.py +30 -14
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/LICENSE +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/_lifecycle.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: log-foundry
|
|
3
|
-
Version: 0.10.2.
|
|
3
|
+
Version: 0.10.2.dev45
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -68,6 +68,25 @@ pip install log-foundry # core, zero dependencies
|
|
|
68
68
|
pip install 'log-foundry[aws]' # + boto3 for the SQS/SNS/Kinesis/Firehose sinks
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
> **Breaking in `1.0.0`.** Three public shapes change once, before the API is frozen under
|
|
72
|
+
> semantic versioning, because none of them could be changed afterwards without a major version:
|
|
73
|
+
>
|
|
74
|
+
> - **`health()` and `sink.losses()` return frozen dataclasses**, not `NamedTuple`s. Attribute
|
|
75
|
+
> access (`h.dropped`, `losses.failed`) is unchanged and is the whole contract; `len(h)`,
|
|
76
|
+
> `h[0]` and `queued, dropped, failed = health()` now raise `TypeError`. `Health` has gained a
|
|
77
|
+
> field in six consecutive specs and gains more — with no positions, that stops being breaking.
|
|
78
|
+
> - **`flush()` returns a `FlushResult` and `continue_trace()` a `ContinueResult`**, each truthy
|
|
79
|
+
> or falsy with a `reason` naming *why*. `if lf.flush():` is unchanged; **`lf.flush() is True`
|
|
80
|
+
> is not** — the result is an object. A one-bit return could not grow a reason later without
|
|
81
|
+
> silently changing what `if flush():` means, which is why it moved now.
|
|
82
|
+
> - **`SQSSink`'s injected client is keyword-only** (`SQSSink(url, client=…)`), **`SentrySink`
|
|
83
|
+
> injects through `client=`** rather than the old `sdk` keyword, with no alias, and the sink attribute the
|
|
84
|
+
> library assigns for interruptible backoff is **`log_foundry_stop_signal`**, not
|
|
85
|
+
> `stop_signal` — a prefixed name cannot silently overwrite one your own sink already uses.
|
|
86
|
+
>
|
|
87
|
+
> `echo`, `message` and `fields` are reserved parameter names on the emitters; pass fields of
|
|
88
|
+
> those names through `fields={...}`, which also takes keys that are not Python identifiers.
|
|
89
|
+
|
|
71
90
|
> **Renamed in 0.2.0: `log_forge` → `log_foundry`.** The import package now matches the
|
|
72
91
|
> distribution name — `pip install log-foundry`, then `import log_foundry`. If you are on
|
|
73
92
|
> `0.1.x`, update your imports; there is no compatibility shim. The project was originally
|
|
@@ -405,7 +424,7 @@ def handler(event, context):
|
|
|
405
424
|
|
|
406
425
|
| Call | Does |
|
|
407
426
|
|---|---|
|
|
408
|
-
| `continue_trace(traceparent=None, *, trace_id=None, parent_span_id=None, baggage=None)` | Adopt an inbound context. `
|
|
427
|
+
| `continue_trace(traceparent=None, *, trace_id=None, parent_span_id=None, baggage=None)` | Adopt an inbound context. Returns a `ContinueResult`: truthy if adopted, else falsy with `reason` of `"nothing-supplied"` or `"rejected"`. The verdict is about the **trace context** — `baggage=` is merged independently and does not make it truthy. Never raises. |
|
|
409
428
|
| `current_traceparent()` | This span as a W3C `traceparent` string, or `None` if no span is active. |
|
|
410
429
|
| `current_trace_context()` | `(trace_id, span_id)`, for when moving two fields beats moving a string. |
|
|
411
430
|
| `current_baggage_header()` | Current baggage in W3C `baggage` format (`""` when empty). |
|
|
@@ -937,11 +956,12 @@ by design: a later level call is *refused* at the closed sink and announced on s
|
|
|
937
956
|
guards its own post-close state — rather than queued where nothing will drain it, and those are not
|
|
938
957
|
the same claim. A stateless sink such as the default `StdoutSink` still accepts it.
|
|
939
958
|
|
|
940
|
-
Read a snapshot by attribute (`h.dropped`), as above
|
|
941
|
-
|
|
942
|
-
(`
|
|
943
|
-
|
|
944
|
-
|
|
959
|
+
Read a snapshot **by attribute** (`h.dropped`), as above — that is the whole contract. `Health`
|
|
960
|
+
and `SinkLosses` are frozen dataclasses, so `len(h)`, `h[0]` and `queued, dropped, failed =
|
|
961
|
+
health()` all raise `TypeError`. They were `NamedTuple`s before `1.0.0` and the tuple shape is
|
|
962
|
+
deliberately gone: `Health` has gained a field in six consecutive specs and gains more, and every
|
|
963
|
+
one of those had to argue that the positions before it were undisturbed. There are no positions to
|
|
964
|
+
disturb now, and adding a field is not a breaking change.
|
|
945
965
|
|
|
946
966
|
`dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
|
|
947
967
|
abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
|
|
@@ -955,12 +975,12 @@ which one you want depends on whether the process is about to end:
|
|
|
955
975
|
```python
|
|
956
976
|
import log_foundry as lf
|
|
957
977
|
|
|
958
|
-
lf.flush() # drain to the sink and keep going;
|
|
978
|
+
lf.flush() # drain to the sink and keep going; truthy when everything landed
|
|
959
979
|
lf.shutdown() # drain, close the sink, and stop for good; blocks until drained (30s cap)
|
|
960
980
|
```
|
|
961
981
|
|
|
962
982
|
Both are bounded, because both can be called somewhere with a deadline. `flush(timeout=5.0)`
|
|
963
|
-
|
|
983
|
+
is falsy if the drain did not complete; `shutdown(timeout=30.0)` returns having stopped
|
|
964
984
|
what it could, and reports `health().stopped_reason == "ShutdownTimeout"`. Passing `None` to
|
|
965
985
|
either waits indefinitely, which is unsafe in any environment with an execution deadline.
|
|
966
986
|
|
|
@@ -985,11 +1005,30 @@ docstring states its own worst case.
|
|
|
985
1005
|
explicitly when you need to be certain the tail reached the sink before a fast exit, e.g. at the
|
|
986
1006
|
end of a short script. It is idempotent.
|
|
987
1007
|
|
|
988
|
-
`flush(timeout=5.0)` returns
|
|
989
|
-
drain it forces reached the sink, and so did anything else the worker
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1008
|
+
`flush(timeout=5.0)` returns a **`FlushResult`**, truthy when **nothing was lost while the call
|
|
1009
|
+
was outstanding** — the drain it forces reached the sink, and so did anything else the worker
|
|
1010
|
+
emitted while it waited its turn. A truthy result is evidence of delivery, not merely that a drain
|
|
1011
|
+
took place.
|
|
1012
|
+
|
|
1013
|
+
Falsy carries a `reason` saying which of five things happened, because they need different fixes:
|
|
1014
|
+
|
|
1015
|
+
| `reason` | Means |
|
|
1016
|
+
|---|---|
|
|
1017
|
+
| `"timed-out"` | The drain did not finish inside your timeout — the destination is slow. |
|
|
1018
|
+
| `"retired"` | `shutdown()` was already called. Your lifecycle is wrong, not the sink. |
|
|
1019
|
+
| `"thread-died"` | The drain thread is gone; see `health().stopped_reason`. |
|
|
1020
|
+
| `"queue-full"` | Backpressure — the queue could not even accept the marker. |
|
|
1021
|
+
| `"abandoned"` | A batch was given up on after its retry budget. The destination is broken. |
|
|
1022
|
+
|
|
1023
|
+
```python
|
|
1024
|
+
result = lf.flush(5.0)
|
|
1025
|
+
if not result:
|
|
1026
|
+
print(f"undelivered: {result.reason}")
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
`if lf.flush():` works exactly as it always did. `lf.flush() is True` does **not** — the result is
|
|
1030
|
+
an object, not a bool. New `reason` values may appear in any release, so treat an unrecognised one
|
|
1031
|
+
as "some other failure" rather than matching exhaustively.
|
|
993
1032
|
|
|
994
1033
|
The window starts when you call it. A batch abandoned *before* that is deliberately not its
|
|
995
1034
|
business: the loss is already counted in `health().failed_batches` and reported on stderr, and
|
|
@@ -32,6 +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
|
+
> **Breaking in `1.0.0`.** Three public shapes change once, before the API is frozen under
|
|
36
|
+
> semantic versioning, because none of them could be changed afterwards without a major version:
|
|
37
|
+
>
|
|
38
|
+
> - **`health()` and `sink.losses()` return frozen dataclasses**, not `NamedTuple`s. Attribute
|
|
39
|
+
> access (`h.dropped`, `losses.failed`) is unchanged and is the whole contract; `len(h)`,
|
|
40
|
+
> `h[0]` and `queued, dropped, failed = health()` now raise `TypeError`. `Health` has gained a
|
|
41
|
+
> field in six consecutive specs and gains more — with no positions, that stops being breaking.
|
|
42
|
+
> - **`flush()` returns a `FlushResult` and `continue_trace()` a `ContinueResult`**, each truthy
|
|
43
|
+
> or falsy with a `reason` naming *why*. `if lf.flush():` is unchanged; **`lf.flush() is True`
|
|
44
|
+
> is not** — the result is an object. A one-bit return could not grow a reason later without
|
|
45
|
+
> silently changing what `if flush():` means, which is why it moved now.
|
|
46
|
+
> - **`SQSSink`'s injected client is keyword-only** (`SQSSink(url, client=…)`), **`SentrySink`
|
|
47
|
+
> injects through `client=`** rather than the old `sdk` keyword, with no alias, and the sink attribute the
|
|
48
|
+
> library assigns for interruptible backoff is **`log_foundry_stop_signal`**, not
|
|
49
|
+
> `stop_signal` — a prefixed name cannot silently overwrite one your own sink already uses.
|
|
50
|
+
>
|
|
51
|
+
> `echo`, `message` and `fields` are reserved parameter names on the emitters; pass fields of
|
|
52
|
+
> those names through `fields={...}`, which also takes keys that are not Python identifiers.
|
|
53
|
+
|
|
35
54
|
> **Renamed in 0.2.0: `log_forge` → `log_foundry`.** The import package now matches the
|
|
36
55
|
> distribution name — `pip install log-foundry`, then `import log_foundry`. If you are on
|
|
37
56
|
> `0.1.x`, update your imports; there is no compatibility shim. The project was originally
|
|
@@ -369,7 +388,7 @@ def handler(event, context):
|
|
|
369
388
|
|
|
370
389
|
| Call | Does |
|
|
371
390
|
|---|---|
|
|
372
|
-
| `continue_trace(traceparent=None, *, trace_id=None, parent_span_id=None, baggage=None)` | Adopt an inbound context. `
|
|
391
|
+
| `continue_trace(traceparent=None, *, trace_id=None, parent_span_id=None, baggage=None)` | Adopt an inbound context. Returns a `ContinueResult`: truthy if adopted, else falsy with `reason` of `"nothing-supplied"` or `"rejected"`. The verdict is about the **trace context** — `baggage=` is merged independently and does not make it truthy. Never raises. |
|
|
373
392
|
| `current_traceparent()` | This span as a W3C `traceparent` string, or `None` if no span is active. |
|
|
374
393
|
| `current_trace_context()` | `(trace_id, span_id)`, for when moving two fields beats moving a string. |
|
|
375
394
|
| `current_baggage_header()` | Current baggage in W3C `baggage` format (`""` when empty). |
|
|
@@ -901,11 +920,12 @@ by design: a later level call is *refused* at the closed sink and announced on s
|
|
|
901
920
|
guards its own post-close state — rather than queued where nothing will drain it, and those are not
|
|
902
921
|
the same claim. A stateless sink such as the default `StdoutSink` still accepts it.
|
|
903
922
|
|
|
904
|
-
Read a snapshot by attribute (`h.dropped`), as above
|
|
905
|
-
|
|
906
|
-
(`
|
|
907
|
-
|
|
908
|
-
|
|
923
|
+
Read a snapshot **by attribute** (`h.dropped`), as above — that is the whole contract. `Health`
|
|
924
|
+
and `SinkLosses` are frozen dataclasses, so `len(h)`, `h[0]` and `queued, dropped, failed =
|
|
925
|
+
health()` all raise `TypeError`. They were `NamedTuple`s before `1.0.0` and the tuple shape is
|
|
926
|
+
deliberately gone: `Health` has gained a field in six consecutive specs and gains more, and every
|
|
927
|
+
one of those had to argue that the positions before it were undisturbed. There are no positions to
|
|
928
|
+
disturb now, and adding a field is not a breaking change.
|
|
909
929
|
|
|
910
930
|
`dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
|
|
911
931
|
abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
|
|
@@ -919,12 +939,12 @@ which one you want depends on whether the process is about to end:
|
|
|
919
939
|
```python
|
|
920
940
|
import log_foundry as lf
|
|
921
941
|
|
|
922
|
-
lf.flush() # drain to the sink and keep going;
|
|
942
|
+
lf.flush() # drain to the sink and keep going; truthy when everything landed
|
|
923
943
|
lf.shutdown() # drain, close the sink, and stop for good; blocks until drained (30s cap)
|
|
924
944
|
```
|
|
925
945
|
|
|
926
946
|
Both are bounded, because both can be called somewhere with a deadline. `flush(timeout=5.0)`
|
|
927
|
-
|
|
947
|
+
is falsy if the drain did not complete; `shutdown(timeout=30.0)` returns having stopped
|
|
928
948
|
what it could, and reports `health().stopped_reason == "ShutdownTimeout"`. Passing `None` to
|
|
929
949
|
either waits indefinitely, which is unsafe in any environment with an execution deadline.
|
|
930
950
|
|
|
@@ -949,11 +969,30 @@ docstring states its own worst case.
|
|
|
949
969
|
explicitly when you need to be certain the tail reached the sink before a fast exit, e.g. at the
|
|
950
970
|
end of a short script. It is idempotent.
|
|
951
971
|
|
|
952
|
-
`flush(timeout=5.0)` returns
|
|
953
|
-
drain it forces reached the sink, and so did anything else the worker
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
972
|
+
`flush(timeout=5.0)` returns a **`FlushResult`**, truthy when **nothing was lost while the call
|
|
973
|
+
was outstanding** — the drain it forces reached the sink, and so did anything else the worker
|
|
974
|
+
emitted while it waited its turn. A truthy result is evidence of delivery, not merely that a drain
|
|
975
|
+
took place.
|
|
976
|
+
|
|
977
|
+
Falsy carries a `reason` saying which of five things happened, because they need different fixes:
|
|
978
|
+
|
|
979
|
+
| `reason` | Means |
|
|
980
|
+
|---|---|
|
|
981
|
+
| `"timed-out"` | The drain did not finish inside your timeout — the destination is slow. |
|
|
982
|
+
| `"retired"` | `shutdown()` was already called. Your lifecycle is wrong, not the sink. |
|
|
983
|
+
| `"thread-died"` | The drain thread is gone; see `health().stopped_reason`. |
|
|
984
|
+
| `"queue-full"` | Backpressure — the queue could not even accept the marker. |
|
|
985
|
+
| `"abandoned"` | A batch was given up on after its retry budget. The destination is broken. |
|
|
986
|
+
|
|
987
|
+
```python
|
|
988
|
+
result = lf.flush(5.0)
|
|
989
|
+
if not result:
|
|
990
|
+
print(f"undelivered: {result.reason}")
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
`if lf.flush():` works exactly as it always did. `lf.flush() is True` does **not** — the result is
|
|
994
|
+
an object, not a bool. New `reason` values may appear in any release, so treat an unrecognised one
|
|
995
|
+
as "some other failure" rather than matching exhaustively.
|
|
957
996
|
|
|
958
997
|
The window starts when you call it. A batch abandoned *before* that is deliberately not its
|
|
959
998
|
business: the loss is already counted in `health().failed_batches` and reported on stderr, and
|
|
@@ -13,6 +13,7 @@ from log_foundry.context import (
|
|
|
13
13
|
reset_context,
|
|
14
14
|
)
|
|
15
15
|
from log_foundry.decorator import continue_trace, trace
|
|
16
|
+
from log_foundry.results import ContinueResult, FlushResult
|
|
16
17
|
from log_foundry.sinks.base import Sink, SinkDeliveryError, SinkLosses, read_losses
|
|
17
18
|
from log_foundry.worker import DEFAULT_SHUTDOWN_TIMEOUT, Health
|
|
18
19
|
|
|
@@ -22,7 +23,7 @@ except PackageNotFoundError:
|
|
|
22
23
|
__version__ = "0.0.0"
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def flush(timeout: float | None = 5.0) ->
|
|
26
|
+
def flush(timeout: float | None = 5.0) -> FlushResult:
|
|
26
27
|
"""Drains buffered events through the sink without closing it.
|
|
27
28
|
|
|
28
29
|
This is the drain for a process that is frozen rather than exited — an AWS Lambda handler
|
|
@@ -37,10 +38,12 @@ def flush(timeout: float | None = 5.0) -> bool:
|
|
|
37
38
|
"the invocation timed out".
|
|
38
39
|
|
|
39
40
|
Returns:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
A :class:`FlushResult`. Truthy when the events submitted before this call reached the
|
|
42
|
+
sink — so a truthy result means they were delivered, not merely that a drain took place.
|
|
43
|
+
Falsy carries a ``reason`` naming which outcome occurred: ``"timed-out"``, ``"retired"``,
|
|
44
|
+
``"thread-died"``, ``"queue-full"`` or ``"abandoned"``. It is falsy rather than ``False``:
|
|
45
|
+
``if flush():`` is unchanged, but ``flush() is True`` can no longer hold, which is why the
|
|
46
|
+
type had to change before ``1.0.0`` rather than after (SPEC-034 FR-007). Events submitted
|
|
44
47
|
concurrently by another thread may or may not be included, since the caller cannot have
|
|
45
48
|
meant those, and a batch lost before the call belongs to :func:`health`.
|
|
46
49
|
|
|
@@ -158,6 +161,8 @@ def shutdown(timeout: float | None = DEFAULT_SHUTDOWN_TIMEOUT) -> None:
|
|
|
158
161
|
__all__ = [
|
|
159
162
|
"DEFAULT_SHUTDOWN_TIMEOUT",
|
|
160
163
|
"Config",
|
|
164
|
+
"ContinueResult",
|
|
165
|
+
"FlushResult",
|
|
161
166
|
"Health",
|
|
162
167
|
"Sink",
|
|
163
168
|
"SinkDeliveryError",
|
|
@@ -7,6 +7,7 @@ import atexit
|
|
|
7
7
|
import functools
|
|
8
8
|
import threading
|
|
9
9
|
from collections.abc import Callable
|
|
10
|
+
from dataclasses import replace
|
|
10
11
|
from time import monotonic
|
|
11
12
|
from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
|
|
12
13
|
|
|
@@ -20,6 +21,7 @@ from log_foundry.ids import (
|
|
|
20
21
|
parse_traceparent,
|
|
21
22
|
)
|
|
22
23
|
from log_foundry.model import Span, backfill_baggage, end_event, start_event
|
|
24
|
+
from log_foundry.results import ContinueResult, FlushResult
|
|
23
25
|
from log_foundry.worker import DEFAULT_SHUTDOWN_TIMEOUT, DEFAULT_SWAP_TIMEOUT, Health, Worker
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
@@ -104,7 +106,7 @@ def continue_trace(
|
|
|
104
106
|
trace_id: str | None = None,
|
|
105
107
|
parent_span_id: str | None = None,
|
|
106
108
|
baggage: str | None = None,
|
|
107
|
-
) ->
|
|
109
|
+
) -> ContinueResult:
|
|
108
110
|
"""Adopts an inbound trace context so this process's spans join the caller's trace.
|
|
109
111
|
|
|
110
112
|
Call it on the first line of the entry point: if a span is already open and it is a root,
|
|
@@ -135,31 +137,52 @@ def continue_trace(
|
|
|
135
137
|
losing the trace join because one field was malformed is worse.
|
|
136
138
|
|
|
137
139
|
Returns:
|
|
138
|
-
|
|
140
|
+
A :class:`ContinueResult`. Truthy when a context was adopted; falsy with a ``reason`` of
|
|
141
|
+
``"nothing-supplied"`` when no argument carried one, or ``"rejected"`` when something was
|
|
142
|
+
supplied and was malformed — two outcomes that read identically as ``False`` today, where
|
|
143
|
+
the second is a caller bug and the first is often a deliberate "continue if there is one".
|
|
144
|
+
Falsy also means a fresh
|
|
139
145
|
trace is in use. Supplying nothing at all is a silent no-op rather than a rejection,
|
|
140
146
|
since a caller who did not propagate a header would otherwise get a line per
|
|
141
147
|
invocation.
|
|
142
148
|
|
|
149
|
+
**The verdict describes the trace context and nothing else.** ``baggage=`` is merged
|
|
150
|
+
independently of it (SPEC-014: losing correlating fields is bad, and losing the trace join
|
|
151
|
+
because one field was malformed is worse), so ``continue_trace(baggage=...)`` alone applies
|
|
152
|
+
the baggage and still reports falsy — there was no trace context to adopt. The reason then
|
|
153
|
+
distinguishes the two honestly, because ``"rejected"`` means **exactly** that a rejection
|
|
154
|
+
was announced through ``_diag``: a malformed ``baggage=`` is a rejection, a well-formed one
|
|
155
|
+
is not, and each reads back the way the stderr line does. A first version keyed the reason
|
|
156
|
+
on "was any argument supplied", which reported ``"nothing-supplied"`` for a malformed
|
|
157
|
+
baggage header *while writing the rejection line for it* — the discrimination FR-007 AC-3
|
|
158
|
+
exists to provide, stated backwards.
|
|
159
|
+
|
|
143
160
|
Raises:
|
|
144
161
|
None.
|
|
145
162
|
"""
|
|
146
163
|
adopted: tuple[str, str | None] | None = None
|
|
164
|
+
announced = False
|
|
147
165
|
if traceparent is not None:
|
|
148
166
|
if trace_id is not None or parent_span_id is not None:
|
|
149
167
|
_diag.rejected("both traceparent and explicit ids given; traceparent wins", traceparent)
|
|
150
168
|
parsed = parse_traceparent(traceparent)
|
|
151
169
|
if parsed is None:
|
|
152
170
|
_diag.rejected("unparseable traceparent", traceparent)
|
|
171
|
+
announced = True
|
|
153
172
|
else:
|
|
154
173
|
adopted = parsed
|
|
155
174
|
elif trace_id is not None:
|
|
156
175
|
if not is_valid_trace_id(trace_id):
|
|
157
176
|
_diag.rejected("invalid trace_id", trace_id)
|
|
177
|
+
announced = True
|
|
158
178
|
elif parent_span_id is not None and not is_valid_span_id(parent_span_id):
|
|
159
179
|
_diag.rejected("invalid parent_span_id; joining as a root", parent_span_id)
|
|
160
180
|
adopted = (trace_id, None)
|
|
161
181
|
else:
|
|
162
182
|
adopted = (trace_id, parent_span_id)
|
|
183
|
+
elif parent_span_id is not None:
|
|
184
|
+
_diag.rejected("parent_span_id given with no trace_id to join", parent_span_id)
|
|
185
|
+
announced = True
|
|
163
186
|
|
|
164
187
|
if adopted is not None:
|
|
165
188
|
context.set_adopted_context(*adopted)
|
|
@@ -169,10 +192,13 @@ def continue_trace(
|
|
|
169
192
|
parsed_baggage = context.parse_baggage_header(baggage)
|
|
170
193
|
if parsed_baggage is None:
|
|
171
194
|
_diag.rejected("unusable baggage header", baggage)
|
|
195
|
+
announced = True
|
|
172
196
|
else:
|
|
173
197
|
context.set_baggage(**parsed_baggage)
|
|
174
198
|
|
|
175
|
-
|
|
199
|
+
if adopted is not None:
|
|
200
|
+
return ContinueResult(ok=True)
|
|
201
|
+
return ContinueResult(ok=False, reason="rejected" if announced else "nothing-supplied")
|
|
176
202
|
|
|
177
203
|
|
|
178
204
|
def _reparent_current_span(trace_id: str, parent_span_id: str | None) -> None:
|
|
@@ -636,7 +662,7 @@ def _adopt_declined_swap(new_sink: Sink) -> None:
|
|
|
636
662
|
_orphan_sink = new_sink
|
|
637
663
|
|
|
638
664
|
|
|
639
|
-
def _flush_worker(timeout: float | None = 5.0) ->
|
|
665
|
+
def _flush_worker(timeout: float | None = 5.0) -> FlushResult:
|
|
640
666
|
"""Drains the process worker without retiring it, backing ``flush()`` (SPEC-013 FR-003).
|
|
641
667
|
|
|
642
668
|
This deliberately does not call :func:`_get_worker`: a process that never logged has
|
|
@@ -647,7 +673,9 @@ def _flush_worker(timeout: float | None = 5.0) -> bool:
|
|
|
647
673
|
timeout: Seconds to wait for the drain, or ``None`` to wait indefinitely.
|
|
648
674
|
|
|
649
675
|
Returns:
|
|
650
|
-
|
|
676
|
+
A :class:`FlushResult`, truthy when everything outstanding was delivered and when no
|
|
677
|
+
worker exists — a process that never logged has nothing to drain, so it has lost
|
|
678
|
+
nothing.
|
|
651
679
|
|
|
652
680
|
Raises:
|
|
653
681
|
None. A flush is the call most likely to be made in a ``finally``, so the library must
|
|
@@ -656,11 +684,11 @@ def _flush_worker(timeout: float | None = 5.0) -> bool:
|
|
|
656
684
|
"""
|
|
657
685
|
worker = _worker
|
|
658
686
|
if worker is None:
|
|
659
|
-
return True
|
|
687
|
+
return FlushResult(ok=True)
|
|
660
688
|
try:
|
|
661
689
|
return worker.flush(timeout)
|
|
662
690
|
except Exception:
|
|
663
|
-
return False
|
|
691
|
+
return FlushResult(ok=False, reason="thread-died")
|
|
664
692
|
|
|
665
693
|
|
|
666
694
|
def _worker_health() -> Health:
|
|
@@ -710,7 +738,7 @@ def _worker_health() -> Health:
|
|
|
710
738
|
)
|
|
711
739
|
health = worker.health()
|
|
712
740
|
if _orphan_retired and not health.retired:
|
|
713
|
-
return health
|
|
741
|
+
return replace(health, retired=True)
|
|
714
742
|
return health
|
|
715
743
|
|
|
716
744
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Result types for the two public calls that answered five questions with one bit (SPEC-034)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
__all__ = ["ContinueResult", "FlushResult"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class _Result:
|
|
12
|
+
"""A verdict that reads as a boolean and can say why (SPEC-034 FR-007).
|
|
13
|
+
|
|
14
|
+
``flush()`` returned one bit for five distinct outcomes — timed out, worker retired, drain
|
|
15
|
+
thread died, queue too full for the marker, a batch abandoned — and a Lambda handler needs
|
|
16
|
+
"the worker is retired, my code is wrong" separated from "the sink is slow". A ``NamedTuple``
|
|
17
|
+
cannot be retrofitted here: a non-empty tuple is always truthy, so every ``if flush():``
|
|
18
|
+
would silently start passing.
|
|
19
|
+
|
|
20
|
+
The type exists now, before ``1.0.0``, precisely so that later reasons are additive. It grows
|
|
21
|
+
by new ``reason`` values and never by changing :meth:`__bool__`.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
ok: Whether the operation succeeded, and what ``bool()`` reports.
|
|
25
|
+
reason: A short stable token naming *why* when it did not, or ``None`` when it did. New
|
|
26
|
+
tokens may appear in any release; code should branch on ``bool()`` and treat an unknown
|
|
27
|
+
reason as "some other failure" rather than matching exhaustively.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ok: bool
|
|
31
|
+
reason: str | None = None
|
|
32
|
+
|
|
33
|
+
def __bool__(self) -> bool:
|
|
34
|
+
"""Reports the verdict, so every existing ``if flush():`` keeps its meaning.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
None.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Whether the operation succeeded.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
None.
|
|
44
|
+
"""
|
|
45
|
+
return self.ok
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class FlushResult(_Result):
|
|
50
|
+
"""What :func:`log_foundry.flush` returns.
|
|
51
|
+
|
|
52
|
+
``reason`` is ``None`` on success. The tokens it can carry today are ``"timed-out"``,
|
|
53
|
+
``"retired"``, ``"thread-died"``, ``"queue-full"`` and ``"abandoned"``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ContinueResult(_Result):
|
|
59
|
+
"""What :func:`log_foundry.continue_trace` returns.
|
|
60
|
+
|
|
61
|
+
``reason`` is ``None`` on success. Today it distinguishes ``"nothing-supplied"`` — no
|
|
62
|
+
argument carried a context — from ``"rejected"``, which means something *was* supplied and
|
|
63
|
+
was malformed. Those two read identically as ``False``, and the second is a caller bug where
|
|
64
|
+
the first is often a deliberate "continue if there is one to continue".
|
|
65
|
+
"""
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from abc import abstractmethod
|
|
6
|
-
from
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
7
8
|
|
|
8
9
|
__all__ = ["Sink", "SinkDeliveryError", "SinkLosses", "read_losses"]
|
|
9
10
|
|
|
@@ -19,7 +20,8 @@ class SinkDeliveryError(Exception):
|
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SinkLosses:
|
|
23
25
|
"""What a sink discarded or could not confirm, cumulative for its lifetime (FR-002).
|
|
24
26
|
|
|
25
27
|
``failed`` is an upper bound on loss, not a count of it: a sink that also raises on total
|
|
@@ -5,9 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import queue
|
|
6
6
|
import threading
|
|
7
7
|
import time
|
|
8
|
-
from
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
9
10
|
|
|
10
11
|
from log_foundry import _diag, _lifecycle
|
|
12
|
+
from log_foundry.results import FlushResult
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
from log_foundry.sinks.base import Sink, SinkLosses
|
|
@@ -57,7 +59,8 @@ def _bounded_seconds(timeout: float | None) -> str:
|
|
|
57
59
|
return "?"
|
|
58
60
|
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class Health:
|
|
61
64
|
"""A point-in-time snapshot of the worker's delivery counters (SPEC-017 FR-005).
|
|
62
65
|
|
|
63
66
|
``stopped_reason``, ``sink``, and SPEC-030's three are defaulted and appended in that
|
|
@@ -112,7 +115,10 @@ class Health(NamedTuple):
|
|
|
112
115
|
may have been misrouted, never merely that a close was slow — ``closing_sinks`` is the
|
|
113
116
|
field for that.
|
|
114
117
|
closing_sinks: Swapped-out sinks whose ``close()`` is running *at this instant* — a live
|
|
115
|
-
gauge
|
|
118
|
+
gauge rather than a counter, so it falls as well as rises. ~~the only field here that
|
|
119
|
+
can fall~~ — struck (SPEC-034 AC-2c): ``queued`` falls on every drain. Those two are the
|
|
120
|
+
gauges and the other five integers are monotonic, which is the distinction an operator
|
|
121
|
+
alerting on "any non-zero" needs and which no name here encodes. A close is
|
|
116
122
|
bounded only in how long ``configure()`` waits for it, so this is how a destination
|
|
117
123
|
stuck in ``close()`` becomes visible at all. Reading it non-zero once means a swap just
|
|
118
124
|
happened; reading it non-zero repeatedly means a close is not coming back, and that sink
|
|
@@ -460,7 +466,7 @@ class Worker:
|
|
|
460
466
|
|
|
461
467
|
return read_losses(self.sink)
|
|
462
468
|
|
|
463
|
-
def flush(self, timeout: float | None = 5.0) ->
|
|
469
|
+
def flush(self, timeout: float | None = 5.0) -> FlushResult:
|
|
464
470
|
"""Drains everything submitted before this call through the sink, without stopping.
|
|
465
471
|
|
|
466
472
|
The precise claim is that nothing was abandoned while this call was outstanding: the
|
|
@@ -498,32 +504,42 @@ class Worker:
|
|
|
498
504
|
wait, so the two cannot add up to twice the timeout. ``None`` waits indefinitely.
|
|
499
505
|
|
|
500
506
|
Returns:
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
case used to return True, a
|
|
507
|
+
A :class:`FlushResult`, truthy once the worker has delivered them and otherwise
|
|
508
|
+
falsy with a ``reason``: ``"timed-out"``, ``"retired"``, ``"thread-died"``,
|
|
509
|
+
``"queue-full"``, or ``"abandoned"`` when the drain carrying those events gave up
|
|
510
|
+
after exhausting retries (SPEC-021 FR-001) — that last case used to return True, a
|
|
511
|
+
false success exactly where ``flush()`` matters most. **The inner call carries the
|
|
512
|
+
type too, not only the public ``log_foundry.flush``** (SPEC-034 FR-007 AC-1b): the
|
|
513
|
+
five outcomes are distinguishable only here, so a public wrapper over a bare ``bool``
|
|
514
|
+
could name none of them without guessing.
|
|
505
515
|
|
|
506
516
|
Raises:
|
|
507
517
|
None.
|
|
508
518
|
"""
|
|
509
519
|
with self._lock:
|
|
510
520
|
if self._shutdown_done:
|
|
511
|
-
return False
|
|
521
|
+
return FlushResult(ok=False, reason="retired")
|
|
512
522
|
if not self._thread.is_alive():
|
|
513
|
-
return False
|
|
523
|
+
return FlushResult(ok=False, reason="thread-died")
|
|
514
524
|
with self._lock:
|
|
515
525
|
marker = _FlushMarker(self.failed_batches)
|
|
516
526
|
deadline = None if timeout is None else time.monotonic() + timeout
|
|
517
527
|
try:
|
|
518
528
|
self._queue.put(marker, timeout=timeout)
|
|
519
529
|
except queue.Full:
|
|
520
|
-
return False
|
|
530
|
+
return FlushResult(ok=False, reason="queue-full")
|
|
521
531
|
if self._drain_finished.is_set() or not self._thread.is_alive():
|
|
522
|
-
|
|
532
|
+
answered = marker.event.is_set()
|
|
533
|
+
delivered = answered and marker.delivered
|
|
534
|
+
if delivered:
|
|
535
|
+
return FlushResult(ok=True)
|
|
536
|
+
return FlushResult(ok=False, reason="abandoned" if answered else "thread-died")
|
|
523
537
|
remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
|
|
524
538
|
if not marker.event.wait(remaining):
|
|
525
|
-
return False
|
|
526
|
-
|
|
539
|
+
return FlushResult(ok=False, reason="timed-out")
|
|
540
|
+
if not marker.delivered:
|
|
541
|
+
return FlushResult(ok=False, reason="abandoned")
|
|
542
|
+
return FlushResult(ok=True)
|
|
527
543
|
|
|
528
544
|
def swap_sink(self, new_sink: Sink, timeout: float | None = DEFAULT_SWAP_TIMEOUT) -> bool:
|
|
529
545
|
"""Retargets delivery at a new sink, draining and closing the previous one (FR-003).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{log_foundry-0.10.2.dev44 → log_foundry-0.10.2.dev45}/src/log_foundry/sinks/elasticsearch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|