log-foundry 0.10.2.dev73__tar.gz → 0.10.2.dev75__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.dev73 → log_foundry-0.10.2.dev75}/PKG-INFO +23 -6
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/README.md +22 -5
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/sentry.py +195 -18
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/LICENSE +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/_fork.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/_lifecycle.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/results.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/memory.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/null.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/src/log_foundry/worker.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.dev75
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -706,7 +706,7 @@ lf.configure(sink=ElasticsearchSink("https://es.internal:9200", index="app-logs"
|
|
|
706
706
|
#### SaaS platforms
|
|
707
707
|
|
|
708
708
|
Also HTTP-based. All are zero-dependency **except** `SentrySink`, which prefers the `sentry-sdk`
|
|
709
|
-
(the `sentry` extra) and falls back to raw HTTP envelopes when it
|
|
709
|
+
(the `sentry` extra) and falls back to raw HTTP envelopes when it cannot deliver through one.
|
|
710
710
|
|
|
711
711
|
| Sink | Import from | Extra | Configure |
|
|
712
712
|
|---|---|---|---|
|
|
@@ -714,11 +714,28 @@ Also HTTP-based. All are zero-dependency **except** `SentrySink`, which prefers
|
|
|
714
714
|
| `SplunkHECSink` | `log_foundry.sinks.splunk` | — | `SplunkHECSink(url, token, *, host=None, source="log-foundry")` — HTTP Event Collector |
|
|
715
715
|
| `NewRelicSink` | `log_foundry.sinks.newrelic` | — | `NewRelicSink(api_key, *, region="US")` — `region` is `"US"` or `"EU"` |
|
|
716
716
|
| `HoneycombSink` | `log_foundry.sinks.honeycomb` | — | `HoneycombSink(api_key, dataset, *, url="https://api.honeycomb.io")` |
|
|
717
|
-
| `SentrySink` | `log_foundry.sinks.sentry` | `sentry` | `SentrySink(dsn=None, *, min_level="ERROR")` — sends only `min_level`+ events |
|
|
717
|
+
| `SentrySink` | `log_foundry.sinks.sentry` | `sentry` | `SentrySink(dsn=None, *, min_level="ERROR", backend="auto")` — sends only `min_level`+ events |
|
|
718
718
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
`SentrySink` captures via `sentry_sdk.capture_event` when the SDK can deliver — you initialize it
|
|
720
|
+
yourself with `sentry_sdk.init(...)` — and POSTs Sentry envelopes over HTTP to `dsn=` otherwise.
|
|
721
|
+
With neither a deliverable SDK nor a `dsn=`, a batch is **refused** rather than silently dropped.
|
|
722
|
+
`backend=` decides:
|
|
723
|
+
|
|
724
|
+
| `backend=` | What it uses |
|
|
725
|
+
|---|---|
|
|
726
|
+
| `"auto"` (default) | the SDK when it can deliver, otherwise the HTTP fallback; re-decided on every batch, so an `init(...)` that arrives after the sink was built is picked up |
|
|
727
|
+
| `"sdk"` | the SDK only. A client that cannot deliver **refuses the batch** rather than quietly switching transport |
|
|
728
|
+
| `"http"` | the HTTP fallback only. No SDK is held, consulted or flushed |
|
|
729
|
+
|
|
730
|
+
"Can deliver" means the SDK's client reports itself active *and* holds a transport. An
|
|
731
|
+
uninitialized process, an `init()` with no DSN, and a `close()`d client all fail that — the first
|
|
732
|
+
reports itself inactive, the other two report themselves active with nothing to send through, and
|
|
733
|
+
all three drop events silently.
|
|
734
|
+
|
|
735
|
+
An argument the chosen backend can never use is a `ValueError` rather than a silent ignore:
|
|
736
|
+
`opener=` where no HTTP fallback is built, `client=` under `backend="http"`. Until this release
|
|
737
|
+
`opener=` was accepted and then ignored whenever the SDK happened to import — including when it was
|
|
738
|
+
installed as somebody else's transitive dependency.
|
|
722
739
|
|
|
723
740
|
#### AWS — the durable-buffer path (`aws` extra)
|
|
724
741
|
|
|
@@ -670,7 +670,7 @@ lf.configure(sink=ElasticsearchSink("https://es.internal:9200", index="app-logs"
|
|
|
670
670
|
#### SaaS platforms
|
|
671
671
|
|
|
672
672
|
Also HTTP-based. All are zero-dependency **except** `SentrySink`, which prefers the `sentry-sdk`
|
|
673
|
-
(the `sentry` extra) and falls back to raw HTTP envelopes when it
|
|
673
|
+
(the `sentry` extra) and falls back to raw HTTP envelopes when it cannot deliver through one.
|
|
674
674
|
|
|
675
675
|
| Sink | Import from | Extra | Configure |
|
|
676
676
|
|---|---|---|---|
|
|
@@ -678,11 +678,28 @@ Also HTTP-based. All are zero-dependency **except** `SentrySink`, which prefers
|
|
|
678
678
|
| `SplunkHECSink` | `log_foundry.sinks.splunk` | — | `SplunkHECSink(url, token, *, host=None, source="log-foundry")` — HTTP Event Collector |
|
|
679
679
|
| `NewRelicSink` | `log_foundry.sinks.newrelic` | — | `NewRelicSink(api_key, *, region="US")` — `region` is `"US"` or `"EU"` |
|
|
680
680
|
| `HoneycombSink` | `log_foundry.sinks.honeycomb` | — | `HoneycombSink(api_key, dataset, *, url="https://api.honeycomb.io")` |
|
|
681
|
-
| `SentrySink` | `log_foundry.sinks.sentry` | `sentry` | `SentrySink(dsn=None, *, min_level="ERROR")` — sends only `min_level`+ events |
|
|
681
|
+
| `SentrySink` | `log_foundry.sinks.sentry` | `sentry` | `SentrySink(dsn=None, *, min_level="ERROR", backend="auto")` — sends only `min_level`+ events |
|
|
682
682
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
683
|
+
`SentrySink` captures via `sentry_sdk.capture_event` when the SDK can deliver — you initialize it
|
|
684
|
+
yourself with `sentry_sdk.init(...)` — and POSTs Sentry envelopes over HTTP to `dsn=` otherwise.
|
|
685
|
+
With neither a deliverable SDK nor a `dsn=`, a batch is **refused** rather than silently dropped.
|
|
686
|
+
`backend=` decides:
|
|
687
|
+
|
|
688
|
+
| `backend=` | What it uses |
|
|
689
|
+
|---|---|
|
|
690
|
+
| `"auto"` (default) | the SDK when it can deliver, otherwise the HTTP fallback; re-decided on every batch, so an `init(...)` that arrives after the sink was built is picked up |
|
|
691
|
+
| `"sdk"` | the SDK only. A client that cannot deliver **refuses the batch** rather than quietly switching transport |
|
|
692
|
+
| `"http"` | the HTTP fallback only. No SDK is held, consulted or flushed |
|
|
693
|
+
|
|
694
|
+
"Can deliver" means the SDK's client reports itself active *and* holds a transport. An
|
|
695
|
+
uninitialized process, an `init()` with no DSN, and a `close()`d client all fail that — the first
|
|
696
|
+
reports itself inactive, the other two report themselves active with nothing to send through, and
|
|
697
|
+
all three drop events silently.
|
|
698
|
+
|
|
699
|
+
An argument the chosen backend can never use is a `ValueError` rather than a silent ignore:
|
|
700
|
+
`opener=` where no HTTP fallback is built, `client=` under `backend="http"`. Until this release
|
|
701
|
+
`opener=` was accepted and then ignored whenever the SDK happened to import — including when it was
|
|
702
|
+
installed as somebody else's transitive dependency.
|
|
686
703
|
|
|
687
704
|
#### AWS — the durable-buffer path (`aws` extra)
|
|
688
705
|
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import threading
|
|
7
7
|
import uuid
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Final, Literal, get_args
|
|
9
9
|
from urllib.parse import urlparse
|
|
10
10
|
|
|
11
11
|
from log_foundry import _diag, _lifecycle
|
|
@@ -14,6 +14,17 @@ from log_foundry.sinks.http import HTTPSink
|
|
|
14
14
|
|
|
15
15
|
__all__ = ["SentrySink"]
|
|
16
16
|
|
|
17
|
+
Backend = Literal["auto", "sdk", "http"]
|
|
18
|
+
"""Which transport a :class:`SentrySink` uses. Not exported: callers pass the literals."""
|
|
19
|
+
|
|
20
|
+
_BACKENDS: Final = get_args(Backend)
|
|
21
|
+
|
|
22
|
+
_Selected = Literal["sdk", "http"]
|
|
23
|
+
"""A backend actually chosen for one batch -- never ``"auto"``, which selects rather than is."""
|
|
24
|
+
|
|
25
|
+
_ABSENT: Final = object()
|
|
26
|
+
"""Sentinel telling an absent member from one whose value is ``None`` (SPEC-043 FR-001)."""
|
|
27
|
+
|
|
17
28
|
_LEVEL_RANK = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40, "CRITICAL": 50}
|
|
18
29
|
_SENTRY_LEVEL = {
|
|
19
30
|
"DEBUG": "debug", "INFO": "info", "WARNING": "warning", "ERROR": "error", "CRITICAL": "fatal",
|
|
@@ -23,12 +34,39 @@ _SENTRY_LEVEL = {
|
|
|
23
34
|
class SentrySink:
|
|
24
35
|
"""A :class:`~log_foundry.sinks.base.Sink` that captures qualifying events to Sentry (FR-011).
|
|
25
36
|
|
|
26
|
-
It uses the ``sentry-sdk`` when
|
|
27
|
-
sink so importing this module never requires it.
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
It uses the ``sentry-sdk`` when one is installed *and able to deliver*, imported lazily inside
|
|
38
|
+
the sink so importing this module never requires it. Otherwise it POSTs a Sentry envelope over
|
|
39
|
+
HTTP to the DSN's ingest URL. Only events at or above the configured minimum level are sent.
|
|
40
|
+
|
|
41
|
+
``backend`` selects explicitly and ``"auto"`` is the default (SPEC-043 FR-002). An explicit
|
|
42
|
+
selection is honoured rather than substituted: under ``"sdk"`` a client that cannot deliver is
|
|
43
|
+
refused, never diverted to HTTP, and under ``"http"`` no client is held, consulted or flushed.
|
|
44
|
+
What is built, and what each ``emit`` then does:
|
|
45
|
+
|
|
46
|
+
=========== ========== ====== ============== =========== ==================================
|
|
47
|
+
``backend`` client? DSN? ``self.client`` ``_http`` Per emit
|
|
48
|
+
=========== ========== ====== ============== =========== ==================================
|
|
49
|
+
``auto`` yes yes the client built SDK if it can deliver, else HTTP
|
|
50
|
+
``auto`` yes no the client ``None`` SDK if it can deliver, else refuse
|
|
51
|
+
``auto`` no yes ``None`` built HTTP
|
|
52
|
+
``auto`` no no — — ``ValueError`` at construction
|
|
53
|
+
``sdk`` yes either the client ``None`` SDK if it can deliver, else refuse
|
|
54
|
+
``sdk`` no either — — ``ValueError`` at construction
|
|
55
|
+
``http`` rejected yes ``None`` built HTTP
|
|
56
|
+
``http`` rejected no — — ``ValueError`` at construction
|
|
57
|
+
=========== ========== ====== ============== =========== ==================================
|
|
58
|
+
|
|
59
|
+
"Can deliver" is judged once per ``emit``, so an application that initialises the SDK after
|
|
60
|
+
building this sink starts using it without rebuilding one. An argument whose only consumer is
|
|
61
|
+
a backend this construction will never select is a ``ValueError`` rather than a silent
|
|
62
|
+
ignore — ``opener`` where no HTTP fallback is built, ``client`` under ``"http"``. That is the
|
|
63
|
+
defect this rule comes from: ``opener`` used to be accepted and then ignored whenever the SDK
|
|
64
|
+
imported. ``max_retries`` is deliberately outside the rule, since its default cannot be told
|
|
65
|
+
from an explicit pass of the same value.
|
|
30
66
|
|
|
31
67
|
Attributes:
|
|
68
|
+
client: The SDK object this sink captures through, or ``None`` when the HTTP fallback is
|
|
69
|
+
the only backend it can select.
|
|
32
70
|
sent: Events captured or sent to Sentry.
|
|
33
71
|
skipped: Events below the minimum level, or without a usable level, that were not sent.
|
|
34
72
|
transport_errors: Events whose send raised something other than an already-counted
|
|
@@ -48,16 +86,29 @@ class SentrySink:
|
|
|
48
86
|
dsn: str | None = None,
|
|
49
87
|
*,
|
|
50
88
|
min_level: str = "ERROR",
|
|
89
|
+
backend: Backend = "auto",
|
|
51
90
|
client: Any = None,
|
|
52
91
|
opener: Any = None,
|
|
53
92
|
max_retries: int = 3,
|
|
54
93
|
) -> None:
|
|
55
94
|
"""Selects the SDK or the HTTP-envelope fallback and sets the level floor.
|
|
56
95
|
|
|
96
|
+
The order is deliberate: an unknown ``backend`` is rejected first, then the arguments the
|
|
97
|
+
selection cannot use, then the selections nothing can build. Several constructions trip a
|
|
98
|
+
conflict *and* a refusal — any of them with no DSN, for instance — and each raises
|
|
99
|
+
``ValueError`` either way, so the conflict is reported first because it names an argument
|
|
100
|
+
the caller can drop, where the refusal only says the selection cannot be built.
|
|
101
|
+
|
|
102
|
+
Both backends this construction can select are built here rather than on first use. A
|
|
103
|
+
fallback built lazily would miss the worker's one-shot ``log_foundry_stop_signal`` offer
|
|
104
|
+
and stop being interruptible (SPEC-027), and it would rebind transport state inside
|
|
105
|
+
``emit``, contradicting this class's SPEC-028 exemption.
|
|
106
|
+
|
|
57
107
|
Args:
|
|
58
108
|
dsn: The Sentry DSN. It is required for the fallback, which needs it to know where to
|
|
59
109
|
POST.
|
|
60
110
|
min_level: The lowest level worth sending.
|
|
111
|
+
backend: Which transport to use — ``"auto"``, ``"sdk"`` or ``"http"``.
|
|
61
112
|
client: A ``sentry_sdk``-shaped object to use instead of importing one.
|
|
62
113
|
opener: A ``urlopen``-shaped callable for the fallback, for tests.
|
|
63
114
|
max_retries: Retries the fallback's HTTP transport makes.
|
|
@@ -66,22 +117,51 @@ class SentrySink:
|
|
|
66
117
|
None.
|
|
67
118
|
|
|
68
119
|
Raises:
|
|
69
|
-
ValueError: If
|
|
120
|
+
ValueError: If ``backend`` is not one of the three names; if an argument cannot be used
|
|
121
|
+
by any backend this construction can select; or if the selection cannot be built —
|
|
122
|
+
``"sdk"`` with no client available, ``"http"`` with no DSN, or the default with
|
|
123
|
+
neither.
|
|
70
124
|
"""
|
|
125
|
+
if backend not in _BACKENDS:
|
|
126
|
+
raise ValueError(f"SentrySink backend must be one of {_BACKENDS!r}, not {backend!r}")
|
|
127
|
+
if client is not None and backend == "http":
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"SentrySink(backend='http') never captures through a client; drop client= or "
|
|
130
|
+
"select a backend that can use it"
|
|
131
|
+
)
|
|
132
|
+
if opener is not None and (backend == "sdk" or dsn is None):
|
|
133
|
+
remedy = "drop backend='sdk'" if backend == "sdk" else "pass a dsn"
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"SentrySink builds no HTTP fallback for this construction, so opener= would "
|
|
136
|
+
f"never be called; {remedy}"
|
|
137
|
+
)
|
|
71
138
|
self._dsn = dsn
|
|
139
|
+
self._backend = backend
|
|
72
140
|
self._min_rank = _LEVEL_RANK.get(min_level.upper(), _LEVEL_RANK["ERROR"])
|
|
73
141
|
self.sent = 0
|
|
74
142
|
self.skipped = 0
|
|
75
143
|
self.transport_errors = 0
|
|
76
144
|
self._counter_lock = threading.Lock()
|
|
77
|
-
self.client =
|
|
145
|
+
self.client: Any = None if backend == "http" else (
|
|
146
|
+
client if client is not None else _import_sdk()
|
|
147
|
+
)
|
|
78
148
|
self._http: HTTPSink | None = None
|
|
79
149
|
self._auth_header = ""
|
|
80
|
-
if
|
|
81
|
-
if
|
|
150
|
+
if backend == "sdk":
|
|
151
|
+
if self.client is None:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"SentrySink(backend='sdk') requires the sentry extra or an injected client="
|
|
154
|
+
)
|
|
155
|
+
elif dsn is None:
|
|
156
|
+
if backend == "http":
|
|
157
|
+
raise ValueError(
|
|
158
|
+
"SentrySink(backend='http') requires a dsn for the HTTP-envelope fallback"
|
|
159
|
+
)
|
|
160
|
+
if self.client is None:
|
|
82
161
|
raise ValueError(
|
|
83
162
|
"SentrySink without sentry-sdk requires a dsn for the HTTP-envelope fallback"
|
|
84
163
|
)
|
|
164
|
+
else:
|
|
85
165
|
ingest_url, self._auth_header = _parse_dsn(dsn)
|
|
86
166
|
self._http = HTTPSink(ingest_url, opener=opener, max_retries=max_retries)
|
|
87
167
|
self._stop_signal: threading.Event | None = None
|
|
@@ -94,6 +174,9 @@ class SentrySink:
|
|
|
94
174
|
FR-001). Letting the first failure propagate would hand the worker a batch whose earlier
|
|
95
175
|
events Sentry had already accepted, and the retry would duplicate them.
|
|
96
176
|
|
|
177
|
+
The backend is chosen once, before the loop, so one batch cannot split across transports
|
|
178
|
+
partway through and so the client is probed once rather than per event.
|
|
179
|
+
|
|
97
180
|
Args:
|
|
98
181
|
batch: The events to consider.
|
|
99
182
|
|
|
@@ -101,10 +184,12 @@ class SentrySink:
|
|
|
101
184
|
None.
|
|
102
185
|
|
|
103
186
|
Raises:
|
|
104
|
-
SinkDeliveryError: If every qualifying event failed to land
|
|
105
|
-
|
|
106
|
-
is a
|
|
187
|
+
SinkDeliveryError: If every qualifying event failed to land, which includes the case
|
|
188
|
+
where no backend can deliver at all (SPEC-043 FR-003). An event below the minimum
|
|
189
|
+
level is skipped rather than lost, so a batch of nothing but skipped events is a
|
|
190
|
+
successful emit — there was never anything to deliver.
|
|
107
191
|
"""
|
|
192
|
+
backend = self._select_backend()
|
|
108
193
|
attempted = delivered = 0
|
|
109
194
|
for event in batch:
|
|
110
195
|
if not self._qualifies(event):
|
|
@@ -112,7 +197,7 @@ class SentrySink:
|
|
|
112
197
|
self.skipped += 1
|
|
113
198
|
continue
|
|
114
199
|
attempted += 1
|
|
115
|
-
if not self._capture(event):
|
|
200
|
+
if not self._capture(event, backend):
|
|
116
201
|
continue
|
|
117
202
|
with self._counter_lock:
|
|
118
203
|
self.sent += 1
|
|
@@ -133,7 +218,12 @@ class SentrySink:
|
|
|
133
218
|
interpreter exit got to it.
|
|
134
219
|
|
|
135
220
|
Only the injected-or-imported SDK client has a queue. The ``urllib`` fallback posts an
|
|
136
|
-
envelope per event and holds nothing, so with no client this is correctly a no-op
|
|
221
|
+
envelope per event and holds nothing, so with no client this is correctly a no-op — and
|
|
222
|
+
``backend="http"`` holds none, which is what keeps this from pushing an application's own
|
|
223
|
+
Sentry transport on behalf of a sink that never captures through it.
|
|
224
|
+
|
|
225
|
+
A client the per-emit predicate currently reads as unable to deliver is still flushed: it
|
|
226
|
+
may become usable before the next batch, and flushing one that cannot is a no-op anyway.
|
|
137
227
|
|
|
138
228
|
``Client.flush`` is probed by name, as every optional member the library calls on an object
|
|
139
229
|
it does not own is: a stand-in ``client=`` satisfying only ``capture_event`` stays valid,
|
|
@@ -254,8 +344,20 @@ class SentrySink:
|
|
|
254
344
|
with self._counter_lock:
|
|
255
345
|
return SinkLosses(dropped=0, failed=self.failed + self.transport_errors)
|
|
256
346
|
|
|
257
|
-
def _capture(self, event: dict[str, object]) -> bool:
|
|
258
|
-
"""Sends one event by
|
|
347
|
+
def _capture(self, event: dict[str, object], backend: _Selected | None) -> bool:
|
|
348
|
+
"""Sends one event by the backend ``emit`` resolved for this batch.
|
|
349
|
+
|
|
350
|
+
A ``None`` backend is refused here, before the ``try``, and moves nothing. Letting it fall
|
|
351
|
+
into the envelope branch instead would reach ``_post_envelope``'s assertion, whose
|
|
352
|
+
``AssertionError`` the guard below counts as a ``transport_errors`` and announces through
|
|
353
|
+
``_diag`` — both forbidden for a refusal (SPEC-043 FR-003), because the caller is told by
|
|
354
|
+
the ``SinkDeliveryError`` ``emit`` raises and counting it here would report one loss twice.
|
|
355
|
+
The refusal stays inside the per-event loop so the level filter keeps running ahead of it
|
|
356
|
+
and the raise names the qualifying count.
|
|
357
|
+
|
|
358
|
+
The branch is on the resolved name rather than on ``self.client is not None``: that
|
|
359
|
+
condition is still true under ``"auto"`` when the client cannot deliver, so it would take
|
|
360
|
+
the SDK branch anyway and preserve the very defect this selection exists to fix.
|
|
259
361
|
|
|
260
362
|
One guard covers both branches, catching ``Exception`` rather than an enumerated set.
|
|
261
363
|
Anything escaping here propagates mid-batch and hands the worker a batch Sentry has
|
|
@@ -267,6 +369,7 @@ class SentrySink:
|
|
|
267
369
|
|
|
268
370
|
Args:
|
|
269
371
|
event: The event to send.
|
|
372
|
+
backend: The backend ``emit`` resolved, or ``None`` when nothing can deliver.
|
|
270
373
|
|
|
271
374
|
Returns:
|
|
272
375
|
True when it landed, False when it did not.
|
|
@@ -276,8 +379,10 @@ class SentrySink:
|
|
|
276
379
|
has already counted and announced it, and counting it again would double-report.
|
|
277
380
|
Only the exception type is ever written (arch §6).
|
|
278
381
|
"""
|
|
382
|
+
if backend is None:
|
|
383
|
+
return False
|
|
279
384
|
try:
|
|
280
|
-
if
|
|
385
|
+
if backend == "sdk":
|
|
281
386
|
self.client.capture_event(self._sentry_event(event))
|
|
282
387
|
else:
|
|
283
388
|
self._post_envelope(event)
|
|
@@ -290,6 +395,76 @@ class SentrySink:
|
|
|
290
395
|
return False
|
|
291
396
|
return True
|
|
292
397
|
|
|
398
|
+
def _select_backend(self) -> _Selected | None:
|
|
399
|
+
"""Picks the transport for one batch, or ``None`` when nothing can deliver.
|
|
400
|
+
|
|
401
|
+
An explicit selection is honoured rather than substituted (SPEC-043 FR-002): ``"sdk"``
|
|
402
|
+
against a client that cannot deliver returns ``None`` so the batch is refused, because a
|
|
403
|
+
caller who named a backend and got a different one is this sink's original defect in a
|
|
404
|
+
new place. ``"http"`` never consults the client, which is what keeps a held-but-unusable
|
|
405
|
+
SDK out of the decision entirely.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
None.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
``"sdk"``, ``"http"``, or ``None`` when neither backend can deliver.
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
None.
|
|
415
|
+
"""
|
|
416
|
+
if self._backend == "http":
|
|
417
|
+
return "http"
|
|
418
|
+
if self.client is not None and self._client_can_deliver():
|
|
419
|
+
return "sdk"
|
|
420
|
+
if self._backend == "sdk":
|
|
421
|
+
return None
|
|
422
|
+
return "http" if self._http is not None else None
|
|
423
|
+
|
|
424
|
+
def _client_can_deliver(self) -> bool:
|
|
425
|
+
"""Reports whether the held client has somewhere to send, not merely that it exists.
|
|
426
|
+
|
|
427
|
+
Three states cannot deliver and only one of them reports itself inactive: an
|
|
428
|
+
uninitialised process holds a ``NonRecordingClient`` (``is_active()`` false), while
|
|
429
|
+
``init()`` without a DSN and a client that has been ``close()``d both report themselves
|
|
430
|
+
active with a ``None`` transport and drop every event silently. So the predicate takes
|
|
431
|
+
both members, and it is the transport that binds — ``is_active()`` is a hardcoded class
|
|
432
|
+
discriminator, kept because it is the SDK's documented answer and a future client may
|
|
433
|
+
diverge, not because anything here can distinguish it from the transport alone.
|
|
434
|
+
|
|
435
|
+
It descends first: ``__init__`` holds the ``sentry_sdk`` **module**, which publishes
|
|
436
|
+
neither member, so a probe that read the held object would call the defective path usable
|
|
437
|
+
and leave the defect in place. ``is_active`` is *called* rather than read, since a bound
|
|
438
|
+
method is truthy and reading one yields a guard that can never fail; a non-callable
|
|
439
|
+
``is_active`` is treated as absent instead. Absence means usable throughout, which is what
|
|
440
|
+
keeps an injected double, a pre-SPEC-043 client and a pre-2.0 SDK working — hence the
|
|
441
|
+
sentinel, since an absent ``transport`` and a ``None`` one are opposite answers.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
None.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
True when the client is worth capturing through.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
None. A probe may never be the reason a batch fails (SPEC-025), so a client that raises
|
|
451
|
+
while being questioned is treated as usable and the fault is announced by type.
|
|
452
|
+
"""
|
|
453
|
+
try:
|
|
454
|
+
target = self.client
|
|
455
|
+
descend = getattr(target, "get_client", None)
|
|
456
|
+
if callable(descend):
|
|
457
|
+
descended = descend()
|
|
458
|
+
if descended is not None:
|
|
459
|
+
target = descended
|
|
460
|
+
is_active = getattr(target, "is_active", None)
|
|
461
|
+
if callable(is_active) and not is_active():
|
|
462
|
+
return False
|
|
463
|
+
return getattr(target, "transport", _ABSENT) is not None
|
|
464
|
+
except Exception as err:
|
|
465
|
+
_diag.absorbed("probing the Sentry client", err, "it is treated as usable")
|
|
466
|
+
return True
|
|
467
|
+
|
|
293
468
|
def _qualifies(self, event: dict[str, object]) -> bool:
|
|
294
469
|
"""Reports whether an event is at or above the configured level floor.
|
|
295
470
|
|
|
@@ -334,7 +509,9 @@ class SentrySink:
|
|
|
334
509
|
"""POSTs one event as a Sentry envelope over the HTTP fallback.
|
|
335
510
|
|
|
336
511
|
The assertion narrows the type for mypy rather than checking at runtime: the HTTP
|
|
337
|
-
transport is set in the constructor whenever the path that reaches here is in use
|
|
512
|
+
transport is set in the constructor whenever the path that reaches here is in use, which
|
|
513
|
+
is true because ``_capture`` refuses a ``None`` backend before reaching this branch and
|
|
514
|
+
``_select_backend`` only answers ``"http"`` where one was built.
|
|
338
515
|
|
|
339
516
|
Args:
|
|
340
517
|
event: The event to send.
|
|
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
|
{log_foundry-0.10.2.dev73 → log_foundry-0.10.2.dev75}/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
|
|
File without changes
|