log-foundry 0.10.2.dev47__tar.gz → 0.10.2.dev49__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.dev47 → log_foundry-0.10.2.dev49}/PKG-INFO +32 -3
  2. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/README.md +31 -2
  3. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/datadog.py +18 -9
  5. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/elasticsearch.py +44 -26
  6. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/honeycomb.py +15 -9
  7. log_foundry-0.10.2.dev49/src/log_foundry/sinks/http.py +949 -0
  8. log_foundry-0.10.2.dev49/src/log_foundry/sinks/loki.py +167 -0
  9. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/newrelic.py +9 -0
  10. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/splunk.py +35 -10
  11. log_foundry-0.10.2.dev47/src/log_foundry/sinks/http.py +0 -417
  12. log_foundry-0.10.2.dev47/src/log_foundry/sinks/loki.py +0 -73
  13. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/LICENSE +0 -0
  14. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/__init__.py +0 -0
  15. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/_diag.py +0 -0
  16. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/_lifecycle.py +0 -0
  17. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/api.py +0 -0
  18. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/config.py +0 -0
  19. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/console.py +0 -0
  20. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/context.py +0 -0
  21. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/decorator.py +0 -0
  22. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/ids.py +0 -0
  23. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/model.py +0 -0
  24. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/py.typed +0 -0
  25. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/results.py +0 -0
  26. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sanitize.py +0 -0
  27. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/__init__.py +0 -0
  28. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/_batch.py +0 -0
  29. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/_chunk.py +0 -0
  30. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/_retry.py +0 -0
  31. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/_socket.py +0 -0
  32. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/_time.py +0 -0
  33. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/base.py +0 -0
  34. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/callback.py +0 -0
  35. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/clickhouse.py +0 -0
  36. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/eventhubs.py +0 -0
  37. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/file.py +0 -0
  38. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/filtering.py +0 -0
  39. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/firehose.py +0 -0
  40. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/kafka.py +0 -0
  41. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/kinesis.py +0 -0
  42. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/logging_sink.py +0 -0
  43. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/logstash.py +0 -0
  44. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/mongodb.py +0 -0
  45. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/multi.py +0 -0
  46. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/nats.py +0 -0
  47. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/postgres.py +0 -0
  48. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/pubsub.py +0 -0
  49. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/rabbitmq.py +0 -0
  50. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/redis.py +0 -0
  51. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/sentry.py +0 -0
  52. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/sns.py +0 -0
  53. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/sqlite.py +0 -0
  54. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/sqs.py +0 -0
  55. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/stdout.py +0 -0
  56. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/syslog.py +0 -0
  57. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/transform.py +0 -0
  58. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/src/log_foundry/sinks/util.py +0 -0
  59. {log_foundry-0.10.2.dev47 → log_foundry-0.10.2.dev49}/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.dev47
3
+ Version: 0.10.2.dev49
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -609,11 +609,40 @@ lf.configure(sink=RotatingFileSink("app.log.jsonl", max_bytes=10_000_000, backup
609
609
 
610
610
  All build on `HTTPSink` (stdlib `urllib`): they POST batches with bounded `429`/`5xx` retry
611
611
  (honoring `Retry-After`) and need **no** extra. On the specialized sinks, `**http_kwargs` forwards
612
- to `HTTPSink` (`headers=`, `auth=`, `gzip=`, `timeout=`, `max_retries=`).
612
+ to `HTTPSink` (`headers=`, `auth=`, `gzip=`, `timeout=`, `max_retries=`, `max_retry_after=`,
613
+ `max_batch_count=`, `max_batch_bytes=`, `opener=`).
614
+
615
+ Each sink **re-chunks** a batch to its destination's limits, so one large `emit` — the whole
616
+ pending backlog at process exit, for instance — becomes several requests the destination will
617
+ accept rather than one it rejects whole. Every subclass sets its own defaults from its vendor's
618
+ documentation where there is one; override with `max_batch_count=` / `max_batch_bytes=`. An event
619
+ too large to be sent on its own is dropped and counted in `health().sink.dropped`.
620
+
621
+ | Sink | `max_batch_count` | `max_batch_bytes` | Where the figure comes from |
622
+ |---|---|---|---|
623
+ | `DatadogSink` | 1,000 | 5,000,000 | both documented by Datadog for the logs intake |
624
+ | `NewRelicSink` | 1,000 | 1,000,000 | the Log API's documented 1 MB (10⁶ bytes) per POST |
625
+ | `HoneycombSink` | 1,000 | 1,000,000 | Honeycomb's documented 1 MB of uncompressed JSON |
626
+ | `ElasticsearchSink` / `OpenSearchSink` | 1,000 | 10,000,000 | chosen below the documented 100 MB `http.max_content_length` |
627
+ | `LokiSink` | 1,000 | 4,000,000 | chosen; Loki's own cap is an operator-tunable server setting |
628
+ | `SplunkHECSink` | 1,000 | 1,000,000 | chosen; Splunk publishes no fixed HEC payload limit |
629
+ | `LogstashSink` (HTTP mode) | 1,000 | 5,000,000 | inherits the generic defaults |
630
+ | `HTTPSink` (generic) | 1,000 | 5,000,000 | chosen, for an endpoint the library knows nothing about |
631
+
632
+ A count of 1,000 is Datadog's published array limit and a conservative default elsewhere — no
633
+ destination in this family documents a smaller one. `DatadogSink` additionally enforces a
634
+ 1,000,000-byte limit on a *single* log, which is the one case where a destination's per-event cap
635
+ is stricter than its per-request one.
636
+
637
+ Two consequences worth knowing. An event too large to be sent on its own is **dropped**, not
638
+ attempted — including in `LogstashSink`'s HTTP mode, which previously put any size on the wire.
639
+ And because a request now carries one chunk rather than the whole batch, a partial failure is
640
+ reported through `health().sink` rather than raised: `flush()` returning `True` means the drain
641
+ completed, not that every chunk of it landed.
613
642
 
614
643
  | Sink | Import from | Configure |
615
644
  |---|---|---|
616
- | `HTTPSink` | `log_foundry.sinks.http` | `HTTPSink(url, *, method="POST", headers=None, auth=None, body_format="ndjson", timeout=5.0, gzip=False, max_retries=3)` — generic POST. `auth` is a bearer-token `str` or `(user, pass)` for basic; `body_format` is `"ndjson"` or `"json_array"` |
645
+ | `HTTPSink` | `log_foundry.sinks.http` | `HTTPSink(url, *, method="POST", headers=None, auth=None, body_format="ndjson", timeout=5.0, gzip=False, max_retries=3, max_retry_after=30.0, max_batch_count=None, max_batch_bytes=None, opener=None)` — generic POST. `auth` is a bearer-token `str` or `(user, pass)` for basic; `body_format` is `"ndjson"` or `"json_array"`; the two `max_batch_*` default to `None`, meaning "use this class's limits" from the table below (1,000 / 5,000,000 here); `opener` injects a `urlopen`-shaped callable for tests |
617
646
  | `ElasticsearchSink` | `log_foundry.sinks.elasticsearch` | `ElasticsearchSink(url, *, index, auth=None, **http_kwargs)` — POST to `_bulk`, parsing per-item errors (`.item_errors`) |
618
647
  | `OpenSearchSink` | `log_foundry.sinks.elasticsearch` | same signature as `ElasticsearchSink` (identical bulk protocol) |
619
648
  | `LokiSink` | `log_foundry.sinks.loki` | `LokiSink(url, *, labels=("service", "env", "level"), **http_kwargs)` — Grafana Loki push API |
@@ -573,11 +573,40 @@ lf.configure(sink=RotatingFileSink("app.log.jsonl", max_bytes=10_000_000, backup
573
573
 
574
574
  All build on `HTTPSink` (stdlib `urllib`): they POST batches with bounded `429`/`5xx` retry
575
575
  (honoring `Retry-After`) and need **no** extra. On the specialized sinks, `**http_kwargs` forwards
576
- to `HTTPSink` (`headers=`, `auth=`, `gzip=`, `timeout=`, `max_retries=`).
576
+ to `HTTPSink` (`headers=`, `auth=`, `gzip=`, `timeout=`, `max_retries=`, `max_retry_after=`,
577
+ `max_batch_count=`, `max_batch_bytes=`, `opener=`).
578
+
579
+ Each sink **re-chunks** a batch to its destination's limits, so one large `emit` — the whole
580
+ pending backlog at process exit, for instance — becomes several requests the destination will
581
+ accept rather than one it rejects whole. Every subclass sets its own defaults from its vendor's
582
+ documentation where there is one; override with `max_batch_count=` / `max_batch_bytes=`. An event
583
+ too large to be sent on its own is dropped and counted in `health().sink.dropped`.
584
+
585
+ | Sink | `max_batch_count` | `max_batch_bytes` | Where the figure comes from |
586
+ |---|---|---|---|
587
+ | `DatadogSink` | 1,000 | 5,000,000 | both documented by Datadog for the logs intake |
588
+ | `NewRelicSink` | 1,000 | 1,000,000 | the Log API's documented 1 MB (10⁶ bytes) per POST |
589
+ | `HoneycombSink` | 1,000 | 1,000,000 | Honeycomb's documented 1 MB of uncompressed JSON |
590
+ | `ElasticsearchSink` / `OpenSearchSink` | 1,000 | 10,000,000 | chosen below the documented 100 MB `http.max_content_length` |
591
+ | `LokiSink` | 1,000 | 4,000,000 | chosen; Loki's own cap is an operator-tunable server setting |
592
+ | `SplunkHECSink` | 1,000 | 1,000,000 | chosen; Splunk publishes no fixed HEC payload limit |
593
+ | `LogstashSink` (HTTP mode) | 1,000 | 5,000,000 | inherits the generic defaults |
594
+ | `HTTPSink` (generic) | 1,000 | 5,000,000 | chosen, for an endpoint the library knows nothing about |
595
+
596
+ A count of 1,000 is Datadog's published array limit and a conservative default elsewhere — no
597
+ destination in this family documents a smaller one. `DatadogSink` additionally enforces a
598
+ 1,000,000-byte limit on a *single* log, which is the one case where a destination's per-event cap
599
+ is stricter than its per-request one.
600
+
601
+ Two consequences worth knowing. An event too large to be sent on its own is **dropped**, not
602
+ attempted — including in `LogstashSink`'s HTTP mode, which previously put any size on the wire.
603
+ And because a request now carries one chunk rather than the whole batch, a partial failure is
604
+ reported through `health().sink` rather than raised: `flush()` returning `True` means the drain
605
+ completed, not that every chunk of it landed.
577
606
 
578
607
  | Sink | Import from | Configure |
579
608
  |---|---|---|
580
- | `HTTPSink` | `log_foundry.sinks.http` | `HTTPSink(url, *, method="POST", headers=None, auth=None, body_format="ndjson", timeout=5.0, gzip=False, max_retries=3)` — generic POST. `auth` is a bearer-token `str` or `(user, pass)` for basic; `body_format` is `"ndjson"` or `"json_array"` |
609
+ | `HTTPSink` | `log_foundry.sinks.http` | `HTTPSink(url, *, method="POST", headers=None, auth=None, body_format="ndjson", timeout=5.0, gzip=False, max_retries=3, max_retry_after=30.0, max_batch_count=None, max_batch_bytes=None, opener=None)` — generic POST. `auth` is a bearer-token `str` or `(user, pass)` for basic; `body_format` is `"ndjson"` or `"json_array"`; the two `max_batch_*` default to `None`, meaning "use this class's limits" from the table below (1,000 / 5,000,000 here); `opener` injects a `urlopen`-shaped callable for tests |
581
610
  | `ElasticsearchSink` | `log_foundry.sinks.elasticsearch` | `ElasticsearchSink(url, *, index, auth=None, **http_kwargs)` — POST to `_bulk`, parsing per-item errors (`.item_errors`) |
582
611
  | `OpenSearchSink` | `log_foundry.sinks.elasticsearch` | same signature as `ElasticsearchSink` (identical bulk protocol) |
583
612
  | `LokiSink` | `log_foundry.sinks.loki` | `LokiSink(url, *, labels=("service", "env", "level"), **http_kwargs)` — Grafana Loki push API |
@@ -20,7 +20,7 @@ dependencies = [
20
20
  ]
21
21
 
22
22
  # Optional features. Install with: pip install log-foundry[aws]
23
- version = "0.10.2.dev47"
23
+ version = "0.10.2.dev49"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -20,8 +20,20 @@ class DatadogSink(HTTPSink):
20
20
  It takes **no** transport lock (SPEC-028 FR-002) and **adds no post-close guard**
21
21
  (SPEC-032 FR-003), for the reasons :class:`~log_foundry.sinks.http.HTTPSink` records: there
22
22
  is no transport held and ``close()`` releases nothing.
23
+
24
+ Attributes:
25
+ MAX_BATCH_COUNT: 1,000 — Datadog's documented maximum array size for the logs intake.
26
+ MAX_BATCH_BYTES: 5,000,000 — its documented maximum uncompressed payload.
27
+ MAX_EVENT_BYTES: 1,000,000 — its documented maximum for a *single* log. This is the one
28
+ sink in the family whose per-event limit is stricter than its request limit, so without
29
+ it a 2 MB event passes the 5 MB request budget and is rejected by a limit the budget
30
+ cannot see. All three are the vendor's own figures, from the Logs API's send-logs limits.
23
31
  """
24
32
 
33
+ MAX_BATCH_COUNT = 1000
34
+ MAX_EVENT_BYTES = 1_000_000
35
+ MAX_BATCH_BYTES = 5_000_000
36
+
25
37
  def __init__(
26
38
  self,
27
39
  api_key: str,
@@ -54,22 +66,19 @@ class DatadogSink(HTTPSink):
54
66
  headers=headers, body_format="json_array", **http_kwargs, # type: ignore[arg-type]
55
67
  )
56
68
 
57
- def emit(self, batch: list[dict[str, object]]) -> None:
58
- """POSTs each enriched event as one JSON array (FR-007).
69
+ def _render(self, event: dict[str, object]) -> str:
70
+ """Serializes one enriched entry for the JSON array (FR-007).
59
71
 
60
72
  Args:
61
- batch: The events to ship. An empty batch is a no-op.
73
+ event: The event to enrich and serialize.
62
74
 
63
75
  Returns:
64
- None.
76
+ The serialized entry.
65
77
 
66
78
  Raises:
67
- SinkDeliveryError: If the request was abandoned past the retry bound.
79
+ TypeError: If the event is not JSON-serializable, which ``sanitize`` prevents.
68
80
  """
69
- if not batch:
70
- return
71
- body = json.dumps([self._entry(event) for event in batch]).encode("utf-8")
72
- self._send(body, content_type="application/json")
81
+ return json.dumps(self._entry(event))
73
82
 
74
83
  def _entry(self, event: dict[str, object]) -> dict[str, object]:
75
84
  """Copies an event and applies the configured Datadog enrichment.
@@ -6,8 +6,8 @@ import json
6
6
 
7
7
  from log_foundry import _diag
8
8
  from log_foundry.sinks._batch import usable_results
9
- from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
10
- from log_foundry.sinks.http import HTTPSink
9
+ from log_foundry.sinks.base import SinkLosses
10
+ from log_foundry.sinks.http import HTTPSink, _Item
11
11
 
12
12
  __all__ = ["ElasticsearchSink", "OpenSearchSink"]
13
13
 
@@ -29,8 +29,20 @@ class ElasticsearchSink(HTTPSink):
29
29
  It takes **no** transport lock (SPEC-028 FR-002) and **adds no post-close guard**
30
30
  (SPEC-032 FR-003), for the reasons :class:`~log_foundry.sinks.http.HTTPSink` records: there
31
31
  is no transport held and ``close()`` releases nothing.
32
+
33
+ Attributes:
34
+ MAX_BATCH_COUNT: 1,000 — this library's conservative default; Elasticsearch bounds a bulk
35
+ request by size, not by document count.
36
+ MAX_BATCH_BYTES: 10,000,000 — chosen *below* the documented hard limit rather than at it.
37
+ Elasticsearch "limits the maximum size of a HTTP request to 100mb by default", but its
38
+ bulk guidance is to find a working size by experiment rather than to send the largest
39
+ request the server will accept, and a 100 MB bulk is a poor default for a log shipper.
40
+ Raise it with ``max_batch_bytes=``.
32
41
  """
33
42
 
43
+ MAX_BATCH_COUNT = 1000
44
+ MAX_BATCH_BYTES = 10_000_000
45
+
34
46
  def __init__(self, url: str, *, index: str, auth: str | tuple[str, str] | None = None,
35
47
  **http_kwargs: object) -> None:
36
48
  """Points the sink at a cluster's ``_bulk`` endpoint.
@@ -54,38 +66,44 @@ class ElasticsearchSink(HTTPSink):
54
66
  self.item_errors = 0
55
67
  self.dropped_unadjudicated = 0
56
68
 
57
- def emit(self, batch: list[dict[str, object]]) -> None:
58
- """Builds the ``_bulk`` NDJSON payload, POSTs it, and parses the response items.
69
+ def _render(self, event: dict[str, object]) -> str:
70
+ """Serializes one event as its ``_bulk`` action line and source line.
59
71
 
60
- An abandoned request raises out of ``_send`` (SPEC-026 FR-001): nothing was indexed, so
61
- there is no response to parse and nothing downstream to duplicate.
72
+ Both lines are one item so the pair can never be split across two requests, and so the
73
+ chunker's byte budget charges the action line the wire actually carries.
62
74
 
63
75
  Args:
64
- batch: The events to index. An empty batch is a no-op.
76
+ event: The event to index.
65
77
 
66
78
  Returns:
67
- None.
79
+ The two newline-joined lines, without the trailing newline :meth:`_body` adds.
68
80
 
69
81
  Raises:
70
- SinkDeliveryError: If the request was abandoned, or if every item carried an error so
71
- the ``200`` indexed nothing — a total failure like any other, which must reach the
72
- worker (FR-001). A retry cannot duplicate, and where the cause is permanent the
73
- worker abandons the batch after its bound and records it, which beats a silent
74
- success. A response rejecting some items stays partial and is reported through
75
- :meth:`losses`.
82
+ TypeError: If the event is not JSON-serializable, which ``sanitize`` prevents.
76
83
  """
77
- if not batch:
78
- return
79
- lines: list[str] = []
80
- for event in batch:
81
- lines.append(json.dumps({"index": {"_index": self._index}}))
82
- lines.append(json.dumps(event))
83
- body = ("\n".join(lines) + "\n").encode("utf-8")
84
- payload = self._send(body, content_type="application/x-ndjson")
85
- if self._parse_bulk_response(payload, len(batch)):
86
- raise SinkDeliveryError(
87
- f"{type(self).__name__} indexed none of {len(batch)} event(s)"
88
- )
84
+ action = json.dumps({"index": {"_index": self._index}})
85
+ return f"{action}\n{json.dumps(event)}"
86
+
87
+ def _handle_response(self, payload: bytes, items: list[_Item]) -> bool:
88
+ """Parses the chunk's ``_bulk`` items and reports whether anything was indexed.
89
+
90
+ Returning ``False`` rather than raising is what lets a sibling chunk that succeeded
91
+ stand: :meth:`~log_foundry.sinks.http.HTTPSink.emit` raises only when *every* chunk came
92
+ back empty-handed, which is the total failure the worker's retry exists for, while a
93
+ batch that indexed some of its chunks must not be re-sent whole (SPEC-026 FR-001).
94
+
95
+ Args:
96
+ payload: The response body.
97
+ items: The chunk the response describes.
98
+
99
+ Returns:
100
+ False when every item in the chunk carried an error, True otherwise — including for a
101
+ body this sink cannot read, since the request itself succeeded.
102
+
103
+ Raises:
104
+ None.
105
+ """
106
+ return not self._parse_bulk_response(payload, len(items))
89
107
 
90
108
  def losses(self) -> SinkLosses:
91
109
  """Reports abandoned requests plus server-rejected bulk items (SPEC-026 FR-002).
@@ -18,8 +18,17 @@ class HoneycombSink(HTTPSink):
18
18
  It takes **no** transport lock (SPEC-028 FR-002) and **adds no post-close guard**
19
19
  (SPEC-032 FR-003), for the reasons :class:`~log_foundry.sinks.http.HTTPSink` records: there
20
20
  is no transport held and ``close()`` releases nothing.
21
+
22
+ Attributes:
23
+ MAX_BATCH_COUNT: 1,000 — this library's conservative default, since Honeycomb documents no
24
+ maximum event count for the batch endpoint.
25
+ MAX_BATCH_BYTES: 1,000,000 — Honeycomb's documented 1 MB of uncompressed JSON for the
26
+ Create Events endpoint.
21
27
  """
22
28
 
29
+ MAX_BATCH_COUNT = 1000
30
+ MAX_BATCH_BYTES = 1_000_000
31
+
23
32
  def __init__(
24
33
  self,
25
34
  api_key: str,
@@ -48,19 +57,16 @@ class HoneycombSink(HTTPSink):
48
57
  headers=headers, body_format="json_array", **http_kwargs, # type: ignore[arg-type]
49
58
  )
50
59
 
51
- def emit(self, batch: list[dict[str, object]]) -> None:
52
- """POSTs the batch in Honeycomb's ``[{"data": event}, ...]`` shape (FR-010).
60
+ def _render(self, event: dict[str, object]) -> str:
61
+ """Serializes one event in Honeycomb's ``{"data": event}`` batch shape (FR-010).
53
62
 
54
63
  Args:
55
- batch: The events to ship. An empty batch is a no-op.
64
+ event: The event to wrap and serialize.
56
65
 
57
66
  Returns:
58
- None.
67
+ The serialized entry.
59
68
 
60
69
  Raises:
61
- SinkDeliveryError: If the request was abandoned past the retry bound.
70
+ TypeError: If the event is not JSON-serializable, which ``sanitize`` prevents.
62
71
  """
63
- if not batch:
64
- return
65
- body = json.dumps([{"data": event} for event in batch]).encode("utf-8")
66
- self._send(body, content_type="application/json")
72
+ return json.dumps({"data": event})