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.
- bufferlog-0.1.0/PKG-INFO +219 -0
- bufferlog-0.1.0/README.md +182 -0
- bufferlog-0.1.0/bufferlog/__init__.py +170 -0
- bufferlog-0.1.0/bufferlog/adapters/__init__.py +7 -0
- bufferlog-0.1.0/bufferlog/adapters/base.py +16 -0
- bufferlog-0.1.0/bufferlog/adapters/datadog.py +58 -0
- bufferlog-0.1.0/bufferlog/adapters/splunk.py +55 -0
- bufferlog-0.1.0/bufferlog/adapters/stdout.py +25 -0
- bufferlog-0.1.0/bufferlog/buffer_manager.py +85 -0
- bufferlog-0.1.0/bufferlog/config.py +47 -0
- bufferlog-0.1.0/bufferlog/context.py +47 -0
- bufferlog-0.1.0/bufferlog/control_plane/__init__.py +1 -0
- bufferlog-0.1.0/bufferlog/control_plane/policy_fetcher.py +112 -0
- bufferlog-0.1.0/bufferlog/control_plane/telemetry_reporter.py +118 -0
- bufferlog-0.1.0/bufferlog/flash_controller.py +78 -0
- bufferlog-0.1.0/bufferlog/integrations/__init__.py +82 -0
- bufferlog-0.1.0/bufferlog/log_event.py +50 -0
- bufferlog-0.1.0/bufferlog/middleware/__init__.py +1 -0
- bufferlog-0.1.0/bufferlog/middleware/django_mw.py +85 -0
- bufferlog-0.1.0/bufferlog/middleware/fastapi_mw.py +92 -0
- bufferlog-0.1.0/bufferlog/middleware/flask_mw.py +71 -0
- bufferlog-0.1.0/bufferlog/ring_buffer.py +94 -0
- bufferlog-0.1.0/bufferlog.egg-info/PKG-INFO +219 -0
- bufferlog-0.1.0/bufferlog.egg-info/SOURCES.txt +32 -0
- bufferlog-0.1.0/bufferlog.egg-info/dependency_links.txt +1 -0
- bufferlog-0.1.0/bufferlog.egg-info/requires.txt +19 -0
- bufferlog-0.1.0/bufferlog.egg-info/top_level.txt +1 -0
- bufferlog-0.1.0/pyproject.toml +39 -0
- bufferlog-0.1.0/setup.cfg +4 -0
- bufferlog-0.1.0/tests/test_buffer_manager.py +63 -0
- bufferlog-0.1.0/tests/test_context.py +47 -0
- bufferlog-0.1.0/tests/test_flash_controller.py +65 -0
- bufferlog-0.1.0/tests/test_integration.py +93 -0
- bufferlog-0.1.0/tests/test_ring_buffer.py +64 -0
bufferlog-0.1.0/PKG-INFO
ADDED
|
@@ -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,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
|