sediment-api 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,120 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """
3
+ POST /v1/logs — OTLP/HTTP JSON receiver for coding-agent telemetry.
4
+
5
+ The path is fixed by the OTLP spec (``<endpoint>/v1/logs``), so no
6
+ ``/ingest`` prefix. Translation lives in ``packages/capture`` (one
7
+ translator per agent, self-filtering by event name); this route walks each
8
+ translated fact into the store.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ from typing import Any
16
+
17
+ from fastapi import APIRouter, Depends, HTTPException, Request
18
+ from sediment_capture import parse_otlp_logs
19
+ from sediment_core import FactStore
20
+ from starlette.concurrency import run_in_threadpool
21
+
22
+ from ..config import settings
23
+ from ..deps import get_store, verify_ingest_token
24
+
25
+ logger = logging.getLogger("sediment.api.otlp")
26
+ router = APIRouter(tags=["otlp"])
27
+
28
+
29
+ @router.post("/v1/logs")
30
+ async def ingest_otlp_logs(
31
+ request: Request,
32
+ _: None = Depends(verify_ingest_token),
33
+ store: FactStore = Depends(get_store),
34
+ ) -> dict[str, Any]:
35
+ try:
36
+ payload = await request.json()
37
+ except HTTPException:
38
+ # The body-size middleware's 413 surfaces inside the read — it must
39
+ # stay a 413, not collapse into the 400 below.
40
+ raise
41
+ except Exception as exc:
42
+ # Malformed body: 400 so the exporter drops it rather than retrying.
43
+ raise HTTPException(status_code=400, detail="invalid OTLP/JSON body") from exc
44
+ if not isinstance(payload, dict):
45
+ raise HTTPException(status_code=400, detail="OTLP body must be a JSON object")
46
+
47
+ capture = parse_otlp_logs(payload, org_id=settings.org_id)
48
+ stored_counts = await run_in_threadpool(
49
+ _store_translated_facts,
50
+ store,
51
+ capture.decisions,
52
+ capture.edit_observations,
53
+ capture.rejected_edits,
54
+ capture.retry_linkages,
55
+ )
56
+ record_counts = {
57
+ "received": capture.records_received,
58
+ "translated": capture.records_translated,
59
+ "untranslated": capture.records_untranslated,
60
+ "malformed": capture.records_malformed,
61
+ }
62
+ fact_counts = {
63
+ name: {
64
+ "candidates": len(facts),
65
+ "stored": stored,
66
+ "duplicates": len(facts) - stored,
67
+ }
68
+ for (name, facts), stored in zip(
69
+ (
70
+ ("developer_decisions", capture.decisions),
71
+ ("edit_observations", capture.edit_observations),
72
+ ("rejected_edits", capture.rejected_edits),
73
+ ("retry_linkages", capture.retry_linkages),
74
+ ),
75
+ stored_counts,
76
+ strict=True,
77
+ )
78
+ }
79
+ retry_linkage_skips = {
80
+ reason.value: count for reason, count in capture.retry_linkage_skips.items()
81
+ }
82
+ # A later storage failure can leave a committed prefix. Only completed
83
+ # requests get a receipt; database dedup accounts for that prefix on replay.
84
+ logger.info(
85
+ "otlp_logs_received org_id=%s record_counts=%s malformed_containers=%d"
86
+ " fact_counts=%s retry_linkage_skips=%s",
87
+ settings.org_id,
88
+ json.dumps(record_counts, sort_keys=True),
89
+ capture.malformed_containers,
90
+ json.dumps(fact_counts, sort_keys=True),
91
+ json.dumps(retry_linkage_skips, sort_keys=True),
92
+ extra={
93
+ "org_id": settings.org_id,
94
+ "record_counts": record_counts,
95
+ "malformed_containers": capture.malformed_containers,
96
+ "fact_counts": fact_counts,
97
+ "retry_linkage_skips": retry_linkage_skips,
98
+ },
99
+ )
100
+ # Empty ExportLogsServiceResponse = full success (OTLP/HTTP JSON).
101
+ return {}
102
+
103
+
104
+ def _store_translated_facts(
105
+ store: FactStore,
106
+ decisions: list,
107
+ observations: list,
108
+ rejected: list,
109
+ retry_linkages: list,
110
+ ) -> tuple[int, int, int, int]:
111
+ """Store one translated OTLP request outside the event loop."""
112
+ stored = sum(store.store_decisions(decisions))
113
+ observations_stored = sum(
114
+ store.store_edit_observation(observation) for observation in observations
115
+ )
116
+ rejected_stored = sum(store.store_rejected_edit(edit) for edit in rejected)
117
+ retry_linkages_stored = sum(
118
+ store.store_retry_linkage(linkage) for linkage in retry_linkages
119
+ )
120
+ return stored, observations_stored, rejected_stored, retry_linkages_stored