bufferlog 0.1.0__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 (34) hide show
  1. bufferlog-0.1.0/PKG-INFO +219 -0
  2. bufferlog-0.1.0/README.md +182 -0
  3. bufferlog-0.1.0/bufferlog/__init__.py +170 -0
  4. bufferlog-0.1.0/bufferlog/adapters/__init__.py +7 -0
  5. bufferlog-0.1.0/bufferlog/adapters/base.py +16 -0
  6. bufferlog-0.1.0/bufferlog/adapters/datadog.py +58 -0
  7. bufferlog-0.1.0/bufferlog/adapters/splunk.py +55 -0
  8. bufferlog-0.1.0/bufferlog/adapters/stdout.py +25 -0
  9. bufferlog-0.1.0/bufferlog/buffer_manager.py +85 -0
  10. bufferlog-0.1.0/bufferlog/config.py +47 -0
  11. bufferlog-0.1.0/bufferlog/context.py +47 -0
  12. bufferlog-0.1.0/bufferlog/control_plane/__init__.py +1 -0
  13. bufferlog-0.1.0/bufferlog/control_plane/policy_fetcher.py +112 -0
  14. bufferlog-0.1.0/bufferlog/control_plane/telemetry_reporter.py +118 -0
  15. bufferlog-0.1.0/bufferlog/flash_controller.py +78 -0
  16. bufferlog-0.1.0/bufferlog/integrations/__init__.py +82 -0
  17. bufferlog-0.1.0/bufferlog/log_event.py +50 -0
  18. bufferlog-0.1.0/bufferlog/middleware/__init__.py +1 -0
  19. bufferlog-0.1.0/bufferlog/middleware/django_mw.py +85 -0
  20. bufferlog-0.1.0/bufferlog/middleware/fastapi_mw.py +92 -0
  21. bufferlog-0.1.0/bufferlog/middleware/flask_mw.py +71 -0
  22. bufferlog-0.1.0/bufferlog/ring_buffer.py +94 -0
  23. bufferlog-0.1.0/bufferlog.egg-info/PKG-INFO +219 -0
  24. bufferlog-0.1.0/bufferlog.egg-info/SOURCES.txt +32 -0
  25. bufferlog-0.1.0/bufferlog.egg-info/dependency_links.txt +1 -0
  26. bufferlog-0.1.0/bufferlog.egg-info/requires.txt +19 -0
  27. bufferlog-0.1.0/bufferlog.egg-info/top_level.txt +1 -0
  28. bufferlog-0.1.0/pyproject.toml +39 -0
  29. bufferlog-0.1.0/setup.cfg +4 -0
  30. bufferlog-0.1.0/tests/test_buffer_manager.py +63 -0
  31. bufferlog-0.1.0/tests/test_context.py +47 -0
  32. bufferlog-0.1.0/tests/test_flash_controller.py +65 -0
  33. bufferlog-0.1.0/tests/test_integration.py +93 -0
  34. bufferlog-0.1.0/tests/test_ring_buffer.py +64 -0
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: bufferlog
3
+ Version: 0.1.0
4
+ Summary: BufferLog — Buffer logs in memory, flush only on errors. Save 90%+ on APM costs.
5
+ Author: BufferLog.io
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lehan0328/bufferlog
8
+ Project-URL: Repository, https://github.com/lehan0328/bufferlog
9
+ Project-URL: Issues, https://github.com/lehan0328/bufferlog/issues
10
+ Keywords: logging,apm,buffer,datadog,splunk,observability,cost-reduction
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Logging
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Provides-Extra: flask
23
+ Requires-Dist: flask>=2.0; extra == "flask"
24
+ Provides-Extra: fastapi
25
+ Requires-Dist: fastapi>=0.100; extra == "fastapi"
26
+ Requires-Dist: starlette>=0.27; extra == "fastapi"
27
+ Provides-Extra: django
28
+ Requires-Dist: django>=4.0; extra == "django"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
32
+ Requires-Dist: flask>=2.0; extra == "dev"
33
+ Requires-Dist: fastapi>=0.100; extra == "dev"
34
+ Requires-Dist: starlette>=0.27; extra == "dev"
35
+ Requires-Dist: httpx>=0.24; extra == "dev"
36
+ Requires-Dist: uvicorn>=0.20; extra == "dev"
37
+
38
+ # bufferlog
39
+
40
+ Buffer logs in memory per-request. Flush only on errors. Save 90%+ on APM costs.
41
+
42
+ ## Table of Contents
43
+
44
+ - [Install](#install)
45
+ - [Usage](#usage)
46
+ - [Flask](#flask)
47
+ - [FastAPI](#fastapi)
48
+ - [Django](#django)
49
+ - [Configuration](#configuration)
50
+ - [Control Plane](#control-plane)
51
+ - [Metrics](#metrics)
52
+ - [Shutdown](#shutdown)
53
+ - [License](#license)
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install bufferlog
59
+ ```
60
+
61
+ With framework extras:
62
+
63
+ ```bash
64
+ pip install bufferlog[flask]
65
+ pip install bufferlog[fastapi]
66
+ pip install bufferlog[django]
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ### Flask
72
+
73
+ ```python
74
+ import logging
75
+ from flask import Flask
76
+ from bufferlog import BufferLog
77
+ from bufferlog.adapters import StdOutAdapter
78
+
79
+ app = Flask(__name__)
80
+ bl = BufferLog(adapters=[StdOutAdapter(pretty=True)])
81
+ bl.init_flask(app)
82
+
83
+ logger = logging.getLogger("myapp")
84
+ logger.addHandler(bl.logging_handler())
85
+ logger.setLevel(logging.DEBUG)
86
+
87
+ @app.route("/")
88
+ def index():
89
+ logger.info("Processing request") # Buffered (discarded on 200)
90
+ logger.debug("SQL: SELECT * ...") # Buffered (discarded on 200)
91
+ return "OK" # 200 → logs discarded ($0)
92
+
93
+ @app.route("/fail")
94
+ def fail():
95
+ logger.info("Starting transaction")
96
+ logger.error("Connection lost") # Triggers flush
97
+ return "Error", 500 # 500 → context sent to APM
98
+ ```
99
+
100
+ ### FastAPI
101
+
102
+ ```python
103
+ import logging
104
+ from fastapi import FastAPI
105
+ from bufferlog import BufferLog
106
+ from bufferlog.adapters import StdOutAdapter
107
+ from bufferlog.middleware.fastapi_mw import BufferLogMiddleware
108
+
109
+ app = FastAPI()
110
+ bl = BufferLog(adapters=[StdOutAdapter()])
111
+ app.add_middleware(BufferLogMiddleware, **bl.asgi_kwargs())
112
+
113
+ logger = logging.getLogger("myapp")
114
+ logger.addHandler(bl.logging_handler())
115
+ logger.setLevel(logging.DEBUG)
116
+
117
+ @app.get("/")
118
+ async def index():
119
+ logger.info("Handling request")
120
+ return {"ok": True}
121
+ ```
122
+
123
+ ### Django
124
+
125
+ ```python
126
+ # settings.py
127
+ MIDDLEWARE = [
128
+ "bufferlog.middleware.django_mw.BufferLogDjangoMiddleware",
129
+ # ... other middleware
130
+ ]
131
+
132
+ # apps.py or wsgi.py
133
+ from bufferlog import BufferLog
134
+ from bufferlog.adapters import StdOutAdapter
135
+
136
+ bl = BufferLog(adapters=[StdOutAdapter()])
137
+ bl.init_django()
138
+ ```
139
+
140
+ ## Configuration
141
+
142
+ ```python
143
+ from bufferlog import BufferLog, BufferLogConfig
144
+ from bufferlog.adapters import DatadogAdapter, SplunkAdapter, StdOutAdapter
145
+
146
+ bl = BufferLog(config=BufferLogConfig(
147
+ # Max logs per request buffer. Oldest overwritten when full.
148
+ buffer_capacity=100,
149
+
150
+ # Log levels that trigger immediate flush.
151
+ flush_on_levels=["error", "critical"],
152
+
153
+ # HTTP status codes that trigger flush.
154
+ flush_on_status_codes=[500, 501, 502, 503, 504],
155
+
156
+ # Downstream targets for flushed logs.
157
+ adapters=[
158
+ DatadogAdapter(api_key="your-key"),
159
+ SplunkAdapter(token="your-token", url="https://splunk.example.com"),
160
+ StdOutAdapter(),
161
+ ],
162
+
163
+ # Enable/disable buffering.
164
+ enabled=True,
165
+
166
+ # If True, logs fall through to stderr on adapter failure.
167
+ fail_open=True,
168
+
169
+ # Scrub PII before logs enter the buffer.
170
+ scrubber=lambda msg, meta: (msg.replace("password", "***"), meta),
171
+ ))
172
+ ```
173
+
174
+ ## Control Plane
175
+
176
+ Connect to the BufferLog Control Plane for remote policy management and usage tracking.
177
+
178
+ ```python
179
+ from bufferlog import BufferLog, ControlPlaneConfig
180
+
181
+ bl = BufferLog(
182
+ control_plane=ControlPlaneConfig(
183
+ url="https://control.bufferlog.io",
184
+ api_key="bl_sk_your_key",
185
+ poll_interval_s=60,
186
+ telemetry_interval_s=60,
187
+ )
188
+ )
189
+ ```
190
+
191
+ When connected:
192
+ - **Poll for policy updates** — buffer capacity, sampling rate, flush triggers, and bypass rules can all be changed remotely.
193
+ - **Push usage metrics** — counters (never log data) are sent to power the ROI dashboard.
194
+
195
+ If the control plane is unreachable, the SDK continues with its last known configuration.
196
+
197
+ ## Metrics
198
+
199
+ ```python
200
+ metrics = bl.get_metrics()
201
+ # {
202
+ # "buffers": {"created": 1000, "discarded": 995, "flushed": 5, "active": 0},
203
+ # "flash": {"flush_count": 5, "events_flushed": 45, "adapter_errors": 0}
204
+ # }
205
+ ```
206
+
207
+ ## Shutdown
208
+
209
+ Stop background threads and send a final telemetry report.
210
+
211
+ ```python
212
+ import atexit
213
+
214
+ atexit.register(bl.shutdown)
215
+ ```
216
+
217
+ ## License
218
+
219
+ [MIT](LICENSE)
@@ -0,0 +1,182 @@
1
+ # bufferlog
2
+
3
+ Buffer logs in memory per-request. Flush only on errors. Save 90%+ on APM costs.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Install](#install)
8
+ - [Usage](#usage)
9
+ - [Flask](#flask)
10
+ - [FastAPI](#fastapi)
11
+ - [Django](#django)
12
+ - [Configuration](#configuration)
13
+ - [Control Plane](#control-plane)
14
+ - [Metrics](#metrics)
15
+ - [Shutdown](#shutdown)
16
+ - [License](#license)
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install bufferlog
22
+ ```
23
+
24
+ With framework extras:
25
+
26
+ ```bash
27
+ pip install bufferlog[flask]
28
+ pip install bufferlog[fastapi]
29
+ pip install bufferlog[django]
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Flask
35
+
36
+ ```python
37
+ import logging
38
+ from flask import Flask
39
+ from bufferlog import BufferLog
40
+ from bufferlog.adapters import StdOutAdapter
41
+
42
+ app = Flask(__name__)
43
+ bl = BufferLog(adapters=[StdOutAdapter(pretty=True)])
44
+ bl.init_flask(app)
45
+
46
+ logger = logging.getLogger("myapp")
47
+ logger.addHandler(bl.logging_handler())
48
+ logger.setLevel(logging.DEBUG)
49
+
50
+ @app.route("/")
51
+ def index():
52
+ logger.info("Processing request") # Buffered (discarded on 200)
53
+ logger.debug("SQL: SELECT * ...") # Buffered (discarded on 200)
54
+ return "OK" # 200 → logs discarded ($0)
55
+
56
+ @app.route("/fail")
57
+ def fail():
58
+ logger.info("Starting transaction")
59
+ logger.error("Connection lost") # Triggers flush
60
+ return "Error", 500 # 500 → context sent to APM
61
+ ```
62
+
63
+ ### FastAPI
64
+
65
+ ```python
66
+ import logging
67
+ from fastapi import FastAPI
68
+ from bufferlog import BufferLog
69
+ from bufferlog.adapters import StdOutAdapter
70
+ from bufferlog.middleware.fastapi_mw import BufferLogMiddleware
71
+
72
+ app = FastAPI()
73
+ bl = BufferLog(adapters=[StdOutAdapter()])
74
+ app.add_middleware(BufferLogMiddleware, **bl.asgi_kwargs())
75
+
76
+ logger = logging.getLogger("myapp")
77
+ logger.addHandler(bl.logging_handler())
78
+ logger.setLevel(logging.DEBUG)
79
+
80
+ @app.get("/")
81
+ async def index():
82
+ logger.info("Handling request")
83
+ return {"ok": True}
84
+ ```
85
+
86
+ ### Django
87
+
88
+ ```python
89
+ # settings.py
90
+ MIDDLEWARE = [
91
+ "bufferlog.middleware.django_mw.BufferLogDjangoMiddleware",
92
+ # ... other middleware
93
+ ]
94
+
95
+ # apps.py or wsgi.py
96
+ from bufferlog import BufferLog
97
+ from bufferlog.adapters import StdOutAdapter
98
+
99
+ bl = BufferLog(adapters=[StdOutAdapter()])
100
+ bl.init_django()
101
+ ```
102
+
103
+ ## Configuration
104
+
105
+ ```python
106
+ from bufferlog import BufferLog, BufferLogConfig
107
+ from bufferlog.adapters import DatadogAdapter, SplunkAdapter, StdOutAdapter
108
+
109
+ bl = BufferLog(config=BufferLogConfig(
110
+ # Max logs per request buffer. Oldest overwritten when full.
111
+ buffer_capacity=100,
112
+
113
+ # Log levels that trigger immediate flush.
114
+ flush_on_levels=["error", "critical"],
115
+
116
+ # HTTP status codes that trigger flush.
117
+ flush_on_status_codes=[500, 501, 502, 503, 504],
118
+
119
+ # Downstream targets for flushed logs.
120
+ adapters=[
121
+ DatadogAdapter(api_key="your-key"),
122
+ SplunkAdapter(token="your-token", url="https://splunk.example.com"),
123
+ StdOutAdapter(),
124
+ ],
125
+
126
+ # Enable/disable buffering.
127
+ enabled=True,
128
+
129
+ # If True, logs fall through to stderr on adapter failure.
130
+ fail_open=True,
131
+
132
+ # Scrub PII before logs enter the buffer.
133
+ scrubber=lambda msg, meta: (msg.replace("password", "***"), meta),
134
+ ))
135
+ ```
136
+
137
+ ## Control Plane
138
+
139
+ Connect to the BufferLog Control Plane for remote policy management and usage tracking.
140
+
141
+ ```python
142
+ from bufferlog import BufferLog, ControlPlaneConfig
143
+
144
+ bl = BufferLog(
145
+ control_plane=ControlPlaneConfig(
146
+ url="https://control.bufferlog.io",
147
+ api_key="bl_sk_your_key",
148
+ poll_interval_s=60,
149
+ telemetry_interval_s=60,
150
+ )
151
+ )
152
+ ```
153
+
154
+ When connected:
155
+ - **Poll for policy updates** — buffer capacity, sampling rate, flush triggers, and bypass rules can all be changed remotely.
156
+ - **Push usage metrics** — counters (never log data) are sent to power the ROI dashboard.
157
+
158
+ If the control plane is unreachable, the SDK continues with its last known configuration.
159
+
160
+ ## Metrics
161
+
162
+ ```python
163
+ metrics = bl.get_metrics()
164
+ # {
165
+ # "buffers": {"created": 1000, "discarded": 995, "flushed": 5, "active": 0},
166
+ # "flash": {"flush_count": 5, "events_flushed": 45, "adapter_errors": 0}
167
+ # }
168
+ ```
169
+
170
+ ## Shutdown
171
+
172
+ Stop background threads and send a final telemetry report.
173
+
174
+ ```python
175
+ import atexit
176
+
177
+ atexit.register(bl.shutdown)
178
+ ```
179
+
180
+ ## License
181
+
182
+ [MIT](LICENSE)
@@ -0,0 +1,170 @@
1
+ """
2
+ BufferLog — Python SDK
3
+
4
+ Buffer logs in memory per-request. Flush only on errors. Save 90%+ on APM costs.
5
+
6
+ Usage:
7
+ from bufferlog import BufferLog
8
+ from bufferlog.adapters import StdOutAdapter
9
+
10
+ bl = BufferLog(adapters=[StdOutAdapter(pretty=True)])
11
+ bl.init_flask(app)
12
+
13
+ # Or for FastAPI:
14
+ from bufferlog.middleware.fastapi_mw import BufferLogMiddleware
15
+ app.add_middleware(BufferLogMiddleware, **bl.asgi_kwargs())
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
21
+
22
+ from .config import BufferLogConfig, ControlPlaneConfig
23
+ from .ring_buffer import RingBuffer
24
+ from .log_event import LogEvent, LogLevel
25
+ from .buffer_manager import BufferManager, BufferManagerMetrics
26
+ from .flash_controller import FlashController
27
+ from .adapters import StdOutAdapter
28
+ from .integrations import BufferLogHandler
29
+ from .control_plane.policy_fetcher import PolicyFetcher
30
+ from .control_plane.telemetry_reporter import TelemetryReporter
31
+
32
+ if TYPE_CHECKING:
33
+ from flask import Flask
34
+
35
+ __version__ = "0.1.0"
36
+ __all__ = [
37
+ "BufferLog",
38
+ "BufferLogConfig",
39
+ "ControlPlaneConfig",
40
+ "RingBuffer",
41
+ "LogEvent",
42
+ "LogLevel",
43
+ "BufferManager",
44
+ "BufferManagerMetrics",
45
+ "FlashController",
46
+ "BufferLogHandler",
47
+ "StdOutAdapter",
48
+ "PolicyFetcher",
49
+ "TelemetryReporter",
50
+ ]
51
+
52
+
53
+ class BufferLog:
54
+ """High-level SDK entry point.
55
+
56
+ Creates and wires all components together. The recommended way to
57
+ use the BufferLog Python SDK.
58
+ """
59
+
60
+ def __init__(self, config: Optional[BufferLogConfig] = None, **kwargs: Any) -> None:
61
+ if config is None:
62
+ config = BufferLogConfig(**kwargs)
63
+ self.config = config
64
+
65
+ self.buffer_manager = BufferManager(config.buffer_capacity)
66
+
67
+ adapters = config.adapters if config.adapters else [StdOutAdapter(pretty=True)]
68
+ self.flash_controller = FlashController(
69
+ adapters=adapters,
70
+ fail_open=config.fail_open,
71
+ )
72
+
73
+ self.policy_fetcher: Optional[PolicyFetcher] = None
74
+ self.telemetry_reporter: Optional[TelemetryReporter] = None
75
+
76
+ if config.control_plane:
77
+ self.policy_fetcher = PolicyFetcher(
78
+ url=config.control_plane.url,
79
+ api_key=config.control_plane.api_key,
80
+ interval_s=config.control_plane.poll_interval_s,
81
+ )
82
+ self.telemetry_reporter = TelemetryReporter(
83
+ url=config.control_plane.url,
84
+ api_key=config.control_plane.api_key,
85
+ interval_s=config.control_plane.telemetry_interval_s,
86
+ metrics_provider=lambda: self.get_metrics_dict(),
87
+ )
88
+ self.policy_fetcher.start()
89
+ self.telemetry_reporter.start()
90
+
91
+ # ---- Framework integrations ----
92
+
93
+ def init_flask(self, app: "Flask") -> None:
94
+ """Register BufferLog middleware on a Flask app."""
95
+ from .middleware.flask_mw import init_flask
96
+
97
+ init_flask(app, self.buffer_manager, self.flash_controller, self.config)
98
+
99
+ def init_django(self) -> None:
100
+ """Configure the Django middleware class with this BufferLog instance.
101
+
102
+ After calling this, add 'bufferlog.middleware.django_mw.BufferLogDjangoMiddleware'
103
+ to your MIDDLEWARE list in settings.py.
104
+ """
105
+ from .middleware.django_mw import BufferLogDjangoMiddleware
106
+
107
+ BufferLogDjangoMiddleware._buffer_manager = self.buffer_manager
108
+ BufferLogDjangoMiddleware._flash_controller = self.flash_controller
109
+ BufferLogDjangoMiddleware._config = self.config
110
+
111
+ def asgi_kwargs(self) -> Dict[str, Any]:
112
+ """Return kwargs for adding the ASGI middleware.
113
+
114
+ Usage:
115
+ from bufferlog.middleware.fastapi_mw import BufferLogMiddleware
116
+ app.add_middleware(BufferLogMiddleware, **bl.asgi_kwargs())
117
+ """
118
+ return {
119
+ "buffer_manager": self.buffer_manager,
120
+ "flash_controller": self.flash_controller,
121
+ "config": self.config,
122
+ }
123
+
124
+ # ---- Logging integration ----
125
+
126
+ def logging_handler(self) -> BufferLogHandler:
127
+ """Create a logging.Handler for Python's built-in logging module."""
128
+ return BufferLogHandler(
129
+ flash_controller=self.flash_controller,
130
+ config=self.config,
131
+ )
132
+
133
+ # ---- Metrics ----
134
+
135
+ def get_metrics(self) -> Dict[str, Any]:
136
+ bm = self.buffer_manager.get_metrics()
137
+ return {
138
+ "buffers": {
139
+ "created": bm.created,
140
+ "discarded": bm.discarded,
141
+ "flushed": bm.flushed,
142
+ "active": bm.active,
143
+ },
144
+ "flash": {
145
+ "flush_count": self.flash_controller.flush_count,
146
+ "events_flushed": self.flash_controller.events_flushed,
147
+ "adapter_errors": self.flash_controller.adapter_errors,
148
+ },
149
+ }
150
+
151
+ def get_metrics_dict(self) -> Dict[str, Any]:
152
+ """Flat metrics dict for telemetry reporter."""
153
+ bm = self.buffer_manager.get_metrics()
154
+ return {
155
+ "logs_discarded": bm.discarded,
156
+ "logs_flushed": bm.flushed,
157
+ "requests_success": bm.discarded,
158
+ "requests_error": bm.flushed,
159
+ "buffers_active": bm.active,
160
+ }
161
+
162
+ # ---- Shutdown ----
163
+
164
+ def shutdown(self) -> None:
165
+ """Gracefully stop background tasks."""
166
+ if self.policy_fetcher:
167
+ self.policy_fetcher.stop()
168
+ if self.telemetry_reporter:
169
+ self.telemetry_reporter.send() # Final report
170
+ self.telemetry_reporter.stop()
@@ -0,0 +1,7 @@
1
+ """BufferLog adapters."""
2
+
3
+ from .stdout import StdOutAdapter
4
+ from .datadog import DatadogAdapter
5
+ from .splunk import SplunkAdapter
6
+
7
+ __all__ = ["StdOutAdapter", "DatadogAdapter", "SplunkAdapter"]
@@ -0,0 +1,16 @@
1
+ """BufferLog — Adapter protocol (abstract base)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List, Protocol, runtime_checkable
6
+
7
+ from ..log_event import LogEvent
8
+
9
+
10
+ @runtime_checkable
11
+ class Adapter(Protocol):
12
+ """Interface that all downstream adapters must implement."""
13
+
14
+ def send(self, events: List[LogEvent], context_id: str) -> None:
15
+ """Send a batch of log events to the downstream target."""
16
+ ...
@@ -0,0 +1,58 @@
1
+ """BufferLog — Datadog Adapter. Sends flushed logs to Datadog Logs API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.request
7
+ from typing import List, Optional
8
+
9
+ from ..log_event import LogEvent
10
+
11
+
12
+ class DatadogAdapter:
13
+ """Ships flushed log events to Datadog via the HTTP Logs API."""
14
+
15
+ DEFAULT_URL = "https://http-intake.logs.datadoghq.com/api/v2/logs"
16
+
17
+ def __init__(
18
+ self,
19
+ api_key: str,
20
+ url: Optional[str] = None,
21
+ service: str = "bufferlog",
22
+ source: str = "python",
23
+ ) -> None:
24
+ self._api_key = api_key
25
+ self._url = url or self.DEFAULT_URL
26
+ self._service = service
27
+ self._source = source
28
+
29
+ def send(self, events: List[LogEvent], context_id: str) -> None:
30
+ payload = [
31
+ {
32
+ "message": e.message,
33
+ "ddtags": f"context_id:{context_id}",
34
+ "ddsource": self._source,
35
+ "service": self._service,
36
+ "level": e.to_dict()["level"],
37
+ "timestamp": int(e.timestamp * 1000),
38
+ **(e.metadata or {}),
39
+ }
40
+ for e in events
41
+ ]
42
+
43
+ data = json.dumps(payload).encode("utf-8")
44
+ req = urllib.request.Request(
45
+ self._url,
46
+ data=data,
47
+ headers={
48
+ "Content-Type": "application/json",
49
+ "DD-API-KEY": self._api_key,
50
+ },
51
+ method="POST",
52
+ )
53
+
54
+ try:
55
+ with urllib.request.urlopen(req, timeout=10) as resp:
56
+ resp.read()
57
+ except Exception:
58
+ pass # Fail-open: don't crash the app if Datadog is unreachable
@@ -0,0 +1,55 @@
1
+ """BufferLog — Splunk HEC Adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.request
7
+ from typing import List, Optional
8
+
9
+ from ..log_event import LogEvent
10
+
11
+
12
+ class SplunkAdapter:
13
+ """Ships flushed log events to Splunk via the HTTP Event Collector."""
14
+
15
+ def __init__(
16
+ self,
17
+ token: str,
18
+ url: str,
19
+ source: str = "bufferlog",
20
+ sourcetype: str = "_json",
21
+ index: Optional[str] = None,
22
+ ) -> None:
23
+ self._token = token
24
+ self._url = url.rstrip("/") + "/services/collector/event"
25
+ self._source = source
26
+ self._sourcetype = sourcetype
27
+ self._index = index
28
+
29
+ def send(self, events: List[LogEvent], context_id: str) -> None:
30
+ for event in events:
31
+ payload = {
32
+ "event": event.to_dict(),
33
+ "source": self._source,
34
+ "sourcetype": self._sourcetype,
35
+ "time": event.timestamp,
36
+ }
37
+ if self._index:
38
+ payload["index"] = self._index
39
+
40
+ data = json.dumps(payload).encode("utf-8")
41
+ req = urllib.request.Request(
42
+ self._url,
43
+ data=data,
44
+ headers={
45
+ "Content-Type": "application/json",
46
+ "Authorization": f"Splunk {self._token}",
47
+ },
48
+ method="POST",
49
+ )
50
+
51
+ try:
52
+ with urllib.request.urlopen(req, timeout=10) as resp:
53
+ resp.read()
54
+ except Exception:
55
+ pass # Fail-open