# Vigil APM SDKs (Python + Node)

Drop-in request tracing for apps running on a server that has the **Vigil server agent** installed.
The SDK runs inside your app, batches spans, and posts them to the agent's loopback listener
(`http://127.0.0.1:9111/v1/spans`). The agent aggregates them into endpoints, dependencies and
slow/errored traces in your Vigil dashboard. No pip/npm packages, no accounts, no keys: the agent
is already authenticated to Vigil.

Both SDKs are a single file with zero dependencies and never raise into your app. If the agent is
down, spans are dropped silently and your app is unaffected.

---

## Python (3.8+)

```bash
curl -sSLo vigil_apm.py https://puzaricloud.in/vigil-apm.py     # note: saved as vigil_apm.py so it is importable
```

```python
import vigil_apm
from fastapi import FastAPI

app = FastAPI()
vigil_apm.init(service="api", app=app)          # that's it
```

`init()` accepts: `service`, `agent_url` (default `http://127.0.0.1:9111`), `sample` (0..1, default 1.0),
`capture_sql` (default True), `app` (FastAPI/Starlette/Flask/any ASGI or WSGI callable), `patch` (False to skip
auto-instrumentation). It returns the app to run:

| Framework                          | How                                                                                   |
|------------------------------------|---------------------------------------------------------------------------------------|
| FastAPI / Starlette                | `vigil_apm.init(service="api", app=app)` (adds `VigilMiddleware`)                     |
| Flask                              | `vigil_apm.init(service="web", app=app)` (wraps `app.wsgi_app`)                        |
| Django / bare WSGI                 | `application = vigil_apm.init(service="web", app=get_wsgi_application())`              |
| Bare ASGI (Quart, Litestar, ...)   | `app = vigil_apm.init(service="api", app=app)` or `app.add_middleware(VigilMiddleware)` |
| Scripts / workers                  | `vigil_apm.init(service="worker")` then `with vigil_apm.span("job"): ...`              |

Run `init()` once at startup, before creating HTTP clients / DB engines where possible (patches apply to
classes, so objects created earlier still get instrumented; SQLAlchemy engines pick up listeners either way).

### What gets captured (Python)

| Kind     | Source                                                                              | Span name                       |
|----------|-------------------------------------------------------------------------------------|---------------------------------|
| server   | `VigilMiddleware` (ASGI) / `VigilWSGIMiddleware` (WSGI): method, templated route, status, duration, client ip | `GET /items/{item_id}`          |
| client   | `httpx` (sync + async), `requests`, `urllib.request`, `http.client` (fallback, no double counting) | `GET api.stripe.com/v1/charges` |
| db       | SQLAlchemy engines (`before/after_cursor_execute`, `handle_error`): `db.system`, statement (200 chars), host | `SELECT users`                  |
| cache    | `redis.Redis` / `redis.asyncio.Redis` `execute_command`                             | `redis GET`                     |
| db       | manual: `with vigil_apm.trace_db("postgresql", sql): cur.execute(sql)` (psycopg, sqlite3, pymongo...) | `SELECT users`         |
| internal | manual: `with vigil_apm.span("price-calc"):` / `@vigil_apm.span("price-calc")` (sync or async) | your name                 |

Route detection: FastAPI/Starlette route template (`/items/{item_id}`), Flask rule (`/items/<int:id>` ->
`/items/{id}`), otherwise the path with numeric/uuid/hex segments replaced by `{id}`/`{uuid}`/`{hash}`.
Set `environ["vigil.route"]` (WSGI) or call `vigil_apm.set_route("/x/{id}")` inside a handler to override.

Helpers: `vigil_apm.current_trace_id()` (put it in your log lines), `vigil_apm.traceparent()`,
`vigil_apm.inject_headers(headers)` for clients we do not patch, `vigil_apm.flush()`, `vigil_apm.instrumented()`.

Context propagates with `contextvars`: it follows `await`, `asyncio` tasks, Starlette's threadpool for sync
endpoints, and threads started via `contextvars.copy_context().run(...)`. A plain `threading.Thread` starts a
new trace (use `copy_context`).

---

## Node.js (16+; `fetch` instrumentation needs 18+)

```bash
curl -sSLO https://puzaricloud.in/vigil-apm.js
```

```js
const apm = require('./vigil-apm');
apm.init({ service: 'web', agentUrl: 'http://127.0.0.1:9111', sample: 1.0 });   // first line of your entry file

const express = require('express');          // require frameworks/drivers AFTER init
const app = express();
app.listen(3000);
```

`init(opts)` accepts `service`, `agentUrl`, `sample`, `captureSql`, `patch` (false to skip auto-instrumentation).
CommonJS only; from ESM use `import { createRequire } from 'module'; const apm = createRequire(import.meta.url)('./vigil-apm.js')`.

### What gets captured (Node)

| Kind     | Source                                                                                          | Span name                      |
|----------|-------------------------------------------------------------------------------------------------|--------------------------------|
| server   | every `http.Server` / `https.Server` request (plain node, Express, Fastify, Koa, Nest on express...) from request start to `res 'finish'` | `GET /items/{id}`     |
| client   | `http.request/get`, `https.request/get` (so axios, got, node-fetch, superagent too), global `fetch` (undici) | `GET api.stripe.com/v1/charges` |
| db       | `pg` (`Client.prototype.query`, covers `Pool.query`), `mysql2` (`query`/`execute`, covers the promise API) | `SELECT users`       |
| cache    | `ioredis` (`sendCommand`)                                                                       | `redis GET`                    |
| db       | manual: `await apm.traceDb('mongodb', 'find users', () => coll.find(q).toArray())`              | your statement                 |
| internal | manual: `await apm.span('price-calc', async (s) => { ... })` (sync or async fn)                 | your name                      |

Drivers are patched when they are `require`d (a `Module._load` hook), or immediately if they were loaded before
`init()`. Route detection at response time: Express `req.baseUrl + req.route.path`, Fastify `req.routerPath` /
`req.routeOptions.url`, otherwise templated path. `apm.express()` returns a middleware that records the route
for older Express setups; `apm.setRoute('/x/:id')` overrides inside a handler.

Helpers: `apm.currentTraceId()`, `apm.traceparent()`, `apm.injectHeaders(headers)`, `apm.flush()` (returns a
promise), `apm.instrumented()`. Context uses `AsyncLocalStorage`, so it follows promises, callbacks and timers.
The flush timer is `unref()`ed: the SDK never keeps your process alive; a final flush runs on `beforeExit`.

---

## Environment variables (both SDKs)

| Variable                 | Meaning                                                            |
|--------------------------|--------------------------------------------------------------------|
| `VIGIL_APM_SERVICE`      | service name (fallback when not passed to init; default: script name) |
| `VIGIL_APM_URL`          | agent listener base URL (default `http://127.0.0.1:9111`)          |
| `VIGIL_APM_SAMPLE`       | fraction of traces to record, 0..1 (default 1)                     |
| `VIGIL_APM_CAPTURE_SQL`  | `0` to omit `db.statement` from db spans                            |
| `VIGIL_APM_DISABLED`     | `1` turns the SDK into a no-op                                      |

Explicit `init()` arguments win over environment variables.

## Batching and safety

Spans are buffered in memory (max 5000, newest dropped beyond that) and flushed every 2 s or every 200 spans as
one `POST /v1/spans` with a 1 s timeout. Failures are dropped, never retried, never logged per request. Internal
SDK errors are logged once (`logging` logger `vigil_apm` at WARNING / `console.warn`) and otherwise swallowed.
At most 200 spans are recorded per trace. `db.statement` is truncated to 200 characters; query strings are
stripped from `http.url`. The SDK never traces its own POSTs to the agent.

## Trace propagation (traceparent)

Both SDKs speak W3C Trace Context. Incoming requests with a `traceparent` header
(`00-<trace_id 32 hex>-<parent span_id 16 hex>-<flags>`) join that trace; otherwise a new trace starts. Every
instrumented outgoing HTTP call sends `traceparent`, so a request that fans out through several of your services
(Python -> Node -> Python) shows up in Vigil as one trace with the correct parent chain. This also interoperates
with OpenTelemetry-instrumented services and with proxies that forward the header. Vigil's uptime/cron pings do
not send `traceparent`, so they start their own traces. For clients the SDK does not patch (gRPC, message queues),
call `inject_headers(...)` / `injectHeaders(...)` yourself.

## Limitations

* Python: `requests`/`httpx`/`sqlalchemy`/`redis` are only patched if importable at `init()` time (installed);
  `aiohttp`, `asyncpg`, `pymongo`, Celery are not auto-instrumented (use `trace_db` / `span`). Django route
  templates are not detected (numeric/uuid templating is used).
* Node: `http2`, `mongodb`, `mysql` (v1), `redis` (node-redis), Prisma/Knex (they use `pg`/`mysql2` underneath,
  so those queries are captured, but without ORM-level names) are not auto-instrumented. Fastify raw requests
  may fall back to templated paths on some versions; use `apm.setRoute()` in a hook if needed.
* Streaming responses: a server span ends when the response finishes; a client span ends when response headers
  arrive.
* The agent listener is loopback only; the SDK must run on the same host (or in a container sharing the host
  network / with `VIGIL_APM_URL` pointing at the agent).
