nexusapm 1.0.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.
nexusapm-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NexusAPM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexusapm
3
+ Version: 1.0.0
4
+ Summary: NexusAPM Python auto-instrumentation SDK (Flask / FastAPI)
5
+ Home-page: https://github.com/Intelli-APM/apm/tree/main/agent/nexusapm-python
6
+ Author: NexusAPM
7
+ License: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Framework :: Flask
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Topic :: System :: Monitoring
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests
19
+ Provides-Extra: accurate-memory
20
+ Requires-Dist: psutil; extra == "accurate-memory"
21
+ Dynamic: author
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: provides-extra
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # nexusapm (Python SDK)
34
+
35
+ One-line auto-instrumentation for Flask and FastAPI applications, reporting
36
+ to a [NexusAPM](https://github.com/Intelli-APM/apm) backend.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install nexusapm
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ **Flask:**
47
+ ```python
48
+ import nexusapm
49
+
50
+ nexusapm.init(app, service_name="my-app",
51
+ backend_url="https://your-nexusapm-backend.example.com",
52
+ api_key="your-api-key") # or set NEXUSAPM_API_KEY in the environment
53
+ ```
54
+
55
+ **FastAPI:**
56
+ ```python
57
+ import nexusapm
58
+
59
+ nexusapm.init(app, service_name="my-api", framework="fastapi",
60
+ backend_url="https://your-nexusapm-backend.example.com",
61
+ api_key="your-api-key")
62
+ ```
63
+
64
+ That's it — every request handled after this point is automatically traced.
65
+
66
+ ## What gets captured automatically
67
+
68
+ - **Per request:** route, method, status, duration → sent as a trace
69
+ - **Per request:** response time → sent as a metric
70
+ - **Errors and slow requests** (4xx/5xx, or over 1s) → sent as log lines
71
+ - **Every 10 seconds:** app-level rollups — memory usage, average response
72
+ time, request throughput, error rate
73
+
74
+ ## Distributed tracing across services
75
+
76
+ If this app calls another NexusAPM-instrumented service over HTTP (via the
77
+ `requests` library), the current request's trace ID is automatically
78
+ propagated onto that outgoing call — no extra code needed. The two
79
+ services' spans show up correlated under one trace in the NexusAPM
80
+ dashboard, instead of two disconnected traces.
81
+
82
+ ## Configuration
83
+
84
+ | Option | Required | Default | Notes |
85
+ |----------------|----------|----------------------------------|--------------------------------------------|
86
+ | `service_name` | No | `"unknown-python-service"` | Shows up as this service's name everywhere |
87
+ | `backend_url` | No | `http://localhost:3001` | Your NexusAPM backend's URL |
88
+ | `api_key` | No* | `NEXUSAPM_API_KEY` env var | *Required for any data to actually be accepted — falls back to the env var if not passed explicitly |
89
+ | `framework` | No | `"flask"` | Set to `"fastapi"` for FastAPI apps |
90
+ | `debug` | No | `False` | Logs `[NexusAPM]`-style debug lines |
91
+
92
+ ## Optional: more accurate memory reporting
93
+
94
+ ```bash
95
+ pip install nexusapm[accurate-memory]
96
+ ```
97
+
98
+ Uses `psutil` for real RSS memory instead of the standard-library fallback.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,70 @@
1
+ # nexusapm (Python SDK)
2
+
3
+ One-line auto-instrumentation for Flask and FastAPI applications, reporting
4
+ to a [NexusAPM](https://github.com/Intelli-APM/apm) backend.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install nexusapm
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ **Flask:**
15
+ ```python
16
+ import nexusapm
17
+
18
+ nexusapm.init(app, service_name="my-app",
19
+ backend_url="https://your-nexusapm-backend.example.com",
20
+ api_key="your-api-key") # or set NEXUSAPM_API_KEY in the environment
21
+ ```
22
+
23
+ **FastAPI:**
24
+ ```python
25
+ import nexusapm
26
+
27
+ nexusapm.init(app, service_name="my-api", framework="fastapi",
28
+ backend_url="https://your-nexusapm-backend.example.com",
29
+ api_key="your-api-key")
30
+ ```
31
+
32
+ That's it — every request handled after this point is automatically traced.
33
+
34
+ ## What gets captured automatically
35
+
36
+ - **Per request:** route, method, status, duration → sent as a trace
37
+ - **Per request:** response time → sent as a metric
38
+ - **Errors and slow requests** (4xx/5xx, or over 1s) → sent as log lines
39
+ - **Every 10 seconds:** app-level rollups — memory usage, average response
40
+ time, request throughput, error rate
41
+
42
+ ## Distributed tracing across services
43
+
44
+ If this app calls another NexusAPM-instrumented service over HTTP (via the
45
+ `requests` library), the current request's trace ID is automatically
46
+ propagated onto that outgoing call — no extra code needed. The two
47
+ services' spans show up correlated under one trace in the NexusAPM
48
+ dashboard, instead of two disconnected traces.
49
+
50
+ ## Configuration
51
+
52
+ | Option | Required | Default | Notes |
53
+ |----------------|----------|----------------------------------|--------------------------------------------|
54
+ | `service_name` | No | `"unknown-python-service"` | Shows up as this service's name everywhere |
55
+ | `backend_url` | No | `http://localhost:3001` | Your NexusAPM backend's URL |
56
+ | `api_key` | No* | `NEXUSAPM_API_KEY` env var | *Required for any data to actually be accepted — falls back to the env var if not passed explicitly |
57
+ | `framework` | No | `"flask"` | Set to `"fastapi"` for FastAPI apps |
58
+ | `debug` | No | `False` | Logs `[NexusAPM]`-style debug lines |
59
+
60
+ ## Optional: more accurate memory reporting
61
+
62
+ ```bash
63
+ pip install nexusapm[accurate-memory]
64
+ ```
65
+
66
+ Uses `psutil` for real RSS memory instead of the standard-library fallback.
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,346 @@
1
+ """
2
+ nexusapm — Python auto-instrumentation SDK
3
+ ============================================
4
+ One-line monitoring for Flask and FastAPI apps.
5
+
6
+ Usage (Flask):
7
+ import nexusapm
8
+ nexusapm.init(app, service_name="my-python-app")
9
+
10
+ Usage (FastAPI):
11
+ import nexusapm
12
+ nexusapm.init(app, service_name="my-api", framework="fastapi")
13
+
14
+ Captures automatically, per request:
15
+ - route, method, status, duration -> trace
16
+ - response_time -> metric
17
+ - errors (4xx/5xx) and slow (>1s) -> logs
18
+ And every 10s, app-level rollups:
19
+ - app_memory_mb, app_avg_response_time,
20
+ app_total_requests, app_error_rate
21
+ """
22
+
23
+ import os
24
+ import time
25
+ import threading
26
+ import atexit
27
+ import random
28
+ import string
29
+ import contextvars
30
+ from datetime import datetime, timezone
31
+
32
+ import requests
33
+
34
+ TRACE_ID_HEADER = "X-NexusAPM-Trace-Id"
35
+ SPAN_ID_HEADER = "X-NexusAPM-Span-Id"
36
+
37
+ # Holds (trace_id, span_id) for whichever request is currently being
38
+ # handled on this thread/task — lets an outgoing `requests` call made deep
39
+ # inside a view function find "which request am I part of" automatically.
40
+ # Previously send_trace() always picked a brand-new random trace_id with no
41
+ # relation to any incoming request, so one logical request crossing
42
+ # multiple instrumented services produced N unrelated traces instead of a
43
+ # single correlated one — this is the fix for that.
44
+ _trace_ctx = contextvars.ContextVar("nexusapm_trace_ctx", default=None)
45
+
46
+ try:
47
+ import psutil
48
+ _HAS_PSUTIL = True
49
+ except Exception:
50
+ _HAS_PSUTIL = False
51
+
52
+ # ---- module state ----
53
+ _config = {
54
+ "service_name": "unknown-python-service",
55
+ "backend_url": "http://localhost:3001",
56
+ "api_key": "",
57
+ "debug": False,
58
+ }
59
+ _stats = {"requests": 0, "errors": 0, "total_duration": 0.0}
60
+ _lock = threading.Lock()
61
+
62
+
63
+ def _now_iso():
64
+ return datetime.now(timezone.utc).isoformat()
65
+
66
+
67
+ def _log(msg):
68
+ print(f"[NexusAPM] {msg}")
69
+
70
+
71
+ def _gen_id():
72
+ return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
73
+
74
+
75
+ # ---- senders (fire-and-forget, never block the app) ----
76
+ def _post(path, payload):
77
+ # All ingestion endpoints (/api/metrics, /api/logs, /api/traces) require
78
+ # an API key — this was missing entirely before, so every call from
79
+ # this SDK silently 401'd once those routes started requiring auth.
80
+ headers = {"x-api-key": _config["api_key"]} if _config["api_key"] else {}
81
+
82
+ def _send():
83
+ try:
84
+ requests.post(
85
+ f"{_config['backend_url']}{path}", json=payload, timeout=2, headers=headers
86
+ )
87
+ except Exception as e:
88
+ if _config["debug"]:
89
+ _log(f"send {path} FAILED: {type(e).__name__}: {e}")
90
+ threading.Thread(target=_send, daemon=True).start()
91
+
92
+
93
+ # Announces "the process I'm running in (this PID) calls itself
94
+ # service_name in its own trace data" — lets the backend link this SDK's
95
+ # traced name to whatever name Service Discovery's process scanner
96
+ # independently gave the same underlying process (e.g. "app" for
97
+ # `python app.py`), purely by matching PID. Sent once at startup and then
98
+ # repeated periodically, since Discovery's own scan might not have found
99
+ # this PID yet at the exact moment the SDK boots.
100
+ ANNOUNCE_INTERVAL_SECS = 60
101
+
102
+
103
+ def _announce_identity():
104
+ headers = {"x-api-key": _config["api_key"]} if _config["api_key"] else {}
105
+
106
+ def _send():
107
+ try:
108
+ requests.post(
109
+ f"{_config['backend_url']}/api/discovery/identify",
110
+ json={"pid": os.getpid(), "tracedServiceName": _config["service_name"]},
111
+ timeout=2, headers=headers,
112
+ )
113
+ except Exception as e:
114
+ if _config["debug"]:
115
+ _log(f"identity announce FAILED: {type(e).__name__}: {e}")
116
+ threading.Thread(target=_send, daemon=True).start()
117
+
118
+
119
+ def _identity_reporter():
120
+ while True:
121
+ time.sleep(ANNOUNCE_INTERVAL_SECS)
122
+ _announce_identity()
123
+
124
+
125
+ def send_metric(name, value, unit=""):
126
+ _post("/api/metrics", {
127
+ "service": _config["service_name"],
128
+ "name": name,
129
+ "value": value,
130
+ "unit": unit,
131
+ "timestamp": _now_iso(),
132
+ })
133
+
134
+
135
+ def send_log(level, message):
136
+ _post("/api/logs", {
137
+ "service": _config["service_name"],
138
+ "level": level,
139
+ "message": message,
140
+ "timestamp": _now_iso(),
141
+ })
142
+
143
+
144
+ def send_trace(route, method, duration_ms, status, trace_id=None, span_id=None, parent_span_id=""):
145
+ """Send exactly ONE trace per request. (Bug fix: was sending two.)"""
146
+ _post("/api/traces", {
147
+ "service": _config["service_name"],
148
+ "traceId": trace_id or _gen_id(),
149
+ "spanId": span_id or _gen_id(),
150
+ "parentSpanId": parent_span_id or "",
151
+ "route": route,
152
+ "method": method,
153
+ "duration": duration_ms,
154
+ "status": status,
155
+ "timestamp": _now_iso(),
156
+ "type": "trace",
157
+ })
158
+
159
+
160
+ # ---- per-request recording ----
161
+ def _record(route, method, duration_ms, status, trace_id=None, span_id=None, parent_span_id=""):
162
+ with _lock:
163
+ _stats["requests"] += 1
164
+ _stats["total_duration"] += duration_ms
165
+ if status >= 400:
166
+ _stats["errors"] += 1
167
+
168
+ if _config["debug"]:
169
+ _log(f"{method} {route} -> {status} ({duration_ms:.0f}ms)")
170
+
171
+ send_trace(route, method, duration_ms, status, trace_id, span_id, parent_span_id)
172
+ send_metric("response_time", round(duration_ms, 2), "ms")
173
+
174
+ if status >= 500:
175
+ send_log("ERROR", f"{method} {route} returned {status} in {duration_ms:.0f}ms")
176
+ elif status in (401, 403):
177
+ send_log("WARN", f"Auth failed: {method} {route} returned {status} in {duration_ms:.0f}ms")
178
+ elif status == 429:
179
+ send_log("WARN", f"Rate limited: {method} {route} returned 429 in {duration_ms:.0f}ms")
180
+ elif status >= 400:
181
+ send_log("WARN", f"{method} {route} returned {status} in {duration_ms:.0f}ms")
182
+ # Two-tier slow-request logging — a 1.2s request and an 8s request are
183
+ # very different severities of problem, so don't bucket them the same.
184
+ if duration_ms > 3000:
185
+ send_log("ERROR", f"Very slow request: {method} {route} took {duration_ms:.0f}ms")
186
+ elif duration_ms > 1000:
187
+ send_log("WARN", f"Slow request: {method} {route} took {duration_ms:.0f}ms")
188
+
189
+
190
+ # ---- 10s rollup reporter ----
191
+ ROLLUP_INTERVAL_SECS = 10
192
+
193
+ def _reporter():
194
+ while True:
195
+ time.sleep(ROLLUP_INTERVAL_SECS)
196
+ with _lock:
197
+ # Snapshot then RESET — same fix as the Node SDK: a counter
198
+ # that never resets sends a useless lifetime total each cycle
199
+ # instead of "what happened in the last 10 seconds".
200
+ reqs = _stats["requests"]
201
+ errs = _stats["errors"]
202
+ total = _stats["total_duration"]
203
+ _stats["requests"] = 0
204
+ _stats["errors"] = 0
205
+ _stats["total_duration"] = 0.0
206
+
207
+ avg = (total / reqs) if reqs else 0.0
208
+ err_rate = (errs / reqs * 100) if reqs else 0.0
209
+ req_per_sec = reqs / ROLLUP_INTERVAL_SECS
210
+
211
+ if _HAS_PSUTIL:
212
+ mem_mb = psutil.Process().memory_info().rss / 1024 / 1024
213
+ else:
214
+ import resource
215
+ mem_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
216
+
217
+ send_metric("app_memory_mb", round(mem_mb, 2), "MB")
218
+ send_metric("app_avg_response_time", round(avg, 2), "ms")
219
+ send_metric("http_request_count", reqs, "count")
220
+ send_metric("http_requests_per_second", round(req_per_sec, 2), "req/s")
221
+ send_metric("app_error_rate", round(err_rate, 2), "%")
222
+
223
+ if _config["debug"]:
224
+ _log(f"rollup: mem={mem_mb:.1f}MB avg={avg:.0f}ms reqs={reqs} ({req_per_sec:.1f}/s) errors={errs}")
225
+
226
+
227
+ # ---- framework integrations ----
228
+ def _instrument_flask(app):
229
+ from flask import request, g
230
+
231
+ @app.before_request
232
+ def _before():
233
+ g._nexus_start = time.time()
234
+ # A trace ID on the incoming request means an upstream instrumented
235
+ # service is already mid-trace — continue it instead of starting a
236
+ # new one. No header means this request is the root of a new trace.
237
+ incoming_trace_id = request.headers.get(TRACE_ID_HEADER)
238
+ incoming_span_id = request.headers.get(SPAN_ID_HEADER)
239
+ g._nexus_trace_id = incoming_trace_id or _gen_id()
240
+ g._nexus_span_id = _gen_id()
241
+ g._nexus_parent_span_id = incoming_span_id or ""
242
+ g._nexus_ctx_token = _trace_ctx.set((g._nexus_trace_id, g._nexus_span_id))
243
+
244
+ @app.after_request
245
+ def _after(response):
246
+ start = getattr(g, "_nexus_start", None)
247
+ if start is not None:
248
+ duration_ms = (time.time() - start) * 1000.0
249
+ _record(
250
+ request.path, request.method, duration_ms, response.status_code,
251
+ trace_id=getattr(g, "_nexus_trace_id", None),
252
+ span_id=getattr(g, "_nexus_span_id", None),
253
+ parent_span_id=getattr(g, "_nexus_parent_span_id", ""),
254
+ )
255
+ token = getattr(g, "_nexus_ctx_token", None)
256
+ if token is not None:
257
+ _trace_ctx.reset(token)
258
+ return response
259
+
260
+ _log("Flask instrumentation attached")
261
+
262
+
263
+ def _instrument_fastapi(app):
264
+ from starlette.requests import Request
265
+
266
+ @app.middleware("http")
267
+ async def _mw(request: Request, call_next):
268
+ start = time.time()
269
+ incoming_trace_id = request.headers.get(TRACE_ID_HEADER)
270
+ incoming_span_id = request.headers.get(SPAN_ID_HEADER)
271
+ trace_id = incoming_trace_id or _gen_id()
272
+ span_id = _gen_id()
273
+ token = _trace_ctx.set((trace_id, span_id))
274
+ try:
275
+ response = await call_next(request)
276
+ finally:
277
+ _trace_ctx.reset(token)
278
+ duration_ms = (time.time() - start) * 1000.0
279
+ _record(
280
+ request.url.path, request.method, duration_ms, response.status_code,
281
+ trace_id=trace_id, span_id=span_id, parent_span_id=incoming_span_id or "",
282
+ )
283
+ return response
284
+
285
+ _log("FastAPI instrumentation attached")
286
+
287
+
288
+ # Propagate the current request's trace context onto any outgoing `requests`
289
+ # call the instrumented app makes (e.g. calling another instrumented
290
+ # service) — this is what actually makes tracing "distributed" rather than
291
+ # each service recording its own disconnected spans. Patches only the one
292
+ # low-level method every requests.* helper (get/post/put/...) funnels
293
+ # through, and only adds headers — it never sends anything itself, so it
294
+ # carries none of the "recorded a trace for every outgoing call" risk that
295
+ # the Node SDK's removed http.request patch had.
296
+ _original_request = requests.Session.request
297
+
298
+
299
+ def _traced_request(self, method, url, *args, **kwargs):
300
+ if not (_config["backend_url"] and str(url).startswith(_config["backend_url"])):
301
+ ctx = _trace_ctx.get()
302
+ if ctx is not None:
303
+ headers = kwargs.get("headers") or {}
304
+ headers = dict(headers)
305
+ headers[TRACE_ID_HEADER] = ctx[0]
306
+ headers[SPAN_ID_HEADER] = ctx[1]
307
+ kwargs["headers"] = headers
308
+ return _original_request(self, method, url, *args, **kwargs)
309
+
310
+
311
+ requests.Session.request = _traced_request
312
+
313
+
314
+ # ---- public init ----
315
+ def init(app=None, service_name=None, backend_url=None, api_key=None, framework="flask", debug=False):
316
+ if service_name:
317
+ _config["service_name"] = service_name
318
+ if backend_url:
319
+ _config["backend_url"] = backend_url
320
+ _config["api_key"] = api_key or os.getenv("NEXUSAPM_API_KEY", "")
321
+ _config["debug"] = debug
322
+
323
+ _log(f"initializing for service: {_config['service_name']}")
324
+ if not _config["api_key"] and debug:
325
+ _log("WARNING: no api_key provided — /api/metrics, /api/logs, /api/traces all require one and will return 401")
326
+
327
+ if app is not None:
328
+ if framework == "flask":
329
+ _instrument_flask(app)
330
+ elif framework == "fastapi":
331
+ _instrument_fastapi(app)
332
+ else:
333
+ _log(f"unknown framework '{framework}', no auto-instrumentation")
334
+
335
+ t = threading.Thread(target=_reporter, daemon=True)
336
+ t.start()
337
+
338
+ _announce_identity()
339
+ threading.Thread(target=_identity_reporter, daemon=True).start()
340
+
341
+ send_log("INFO", f"NexusAPM agent started — monitoring {_config['service_name']}")
342
+ atexit.register(
343
+ lambda: send_log("INFO", f"NexusAPM agent stopped — {_config['service_name']}")
344
+ )
345
+
346
+ _log("ready! Monitoring all HTTP requests automatically.")
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexusapm
3
+ Version: 1.0.0
4
+ Summary: NexusAPM Python auto-instrumentation SDK (Flask / FastAPI)
5
+ Home-page: https://github.com/Intelli-APM/apm/tree/main/agent/nexusapm-python
6
+ Author: NexusAPM
7
+ License: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Framework :: Flask
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Topic :: System :: Monitoring
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests
19
+ Provides-Extra: accurate-memory
20
+ Requires-Dist: psutil; extra == "accurate-memory"
21
+ Dynamic: author
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: provides-extra
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # nexusapm (Python SDK)
34
+
35
+ One-line auto-instrumentation for Flask and FastAPI applications, reporting
36
+ to a [NexusAPM](https://github.com/Intelli-APM/apm) backend.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install nexusapm
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ **Flask:**
47
+ ```python
48
+ import nexusapm
49
+
50
+ nexusapm.init(app, service_name="my-app",
51
+ backend_url="https://your-nexusapm-backend.example.com",
52
+ api_key="your-api-key") # or set NEXUSAPM_API_KEY in the environment
53
+ ```
54
+
55
+ **FastAPI:**
56
+ ```python
57
+ import nexusapm
58
+
59
+ nexusapm.init(app, service_name="my-api", framework="fastapi",
60
+ backend_url="https://your-nexusapm-backend.example.com",
61
+ api_key="your-api-key")
62
+ ```
63
+
64
+ That's it — every request handled after this point is automatically traced.
65
+
66
+ ## What gets captured automatically
67
+
68
+ - **Per request:** route, method, status, duration → sent as a trace
69
+ - **Per request:** response time → sent as a metric
70
+ - **Errors and slow requests** (4xx/5xx, or over 1s) → sent as log lines
71
+ - **Every 10 seconds:** app-level rollups — memory usage, average response
72
+ time, request throughput, error rate
73
+
74
+ ## Distributed tracing across services
75
+
76
+ If this app calls another NexusAPM-instrumented service over HTTP (via the
77
+ `requests` library), the current request's trace ID is automatically
78
+ propagated onto that outgoing call — no extra code needed. The two
79
+ services' spans show up correlated under one trace in the NexusAPM
80
+ dashboard, instead of two disconnected traces.
81
+
82
+ ## Configuration
83
+
84
+ | Option | Required | Default | Notes |
85
+ |----------------|----------|----------------------------------|--------------------------------------------|
86
+ | `service_name` | No | `"unknown-python-service"` | Shows up as this service's name everywhere |
87
+ | `backend_url` | No | `http://localhost:3001` | Your NexusAPM backend's URL |
88
+ | `api_key` | No* | `NEXUSAPM_API_KEY` env var | *Required for any data to actually be accepted — falls back to the env var if not passed explicitly |
89
+ | `framework` | No | `"flask"` | Set to `"fastapi"` for FastAPI apps |
90
+ | `debug` | No | `False` | Logs `[NexusAPM]`-style debug lines |
91
+
92
+ ## Optional: more accurate memory reporting
93
+
94
+ ```bash
95
+ pip install nexusapm[accurate-memory]
96
+ ```
97
+
98
+ Uses `psutil` for real RSS memory instead of the standard-library fallback.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ nexusapm/__init__.py
5
+ nexusapm.egg-info/PKG-INFO
6
+ nexusapm.egg-info/SOURCES.txt
7
+ nexusapm.egg-info/dependency_links.txt
8
+ nexusapm.egg-info/requires.txt
9
+ nexusapm.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ requests
2
+
3
+ [accurate-memory]
4
+ psutil
@@ -0,0 +1 @@
1
+ nexusapm
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ from pathlib import Path
2
+ from setuptools import setup, find_packages
3
+
4
+ long_description = (Path(__file__).parent / "README.md").read_text(encoding="utf-8")
5
+
6
+ setup(
7
+ name="nexusapm",
8
+ version="1.0.0",
9
+ description="NexusAPM Python auto-instrumentation SDK (Flask / FastAPI)",
10
+ long_description=long_description,
11
+ long_description_content_type="text/markdown",
12
+ author="NexusAPM",
13
+ license="MIT",
14
+ url="https://github.com/Intelli-APM/apm/tree/main/agent/nexusapm-python",
15
+ packages=find_packages(),
16
+ install_requires=["requests"],
17
+ extras_require={"accurate-memory": ["psutil"]},
18
+ python_requires=">=3.8",
19
+ classifiers=[
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Framework :: Flask",
25
+ "Framework :: FastAPI",
26
+ "Topic :: System :: Monitoring",
27
+ ],
28
+ )