@netgreener/runtime 0.1.0
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.
- package/CHANGELOG.md +16 -0
- package/README.md +28 -0
- package/TUTORIAL.md +495 -0
- package/dist/aggregator.d.ts +43 -0
- package/dist/aggregator.js +270 -0
- package/dist/analyze/analyzeGate.d.ts +44 -0
- package/dist/analyze/analyzeGate.js +114 -0
- package/dist/analyze/apiOverconsumptionDetector.d.ts +13 -0
- package/dist/analyze/apiOverconsumptionDetector.js +75 -0
- package/dist/analyze/astDetectors.d.ts +38 -0
- package/dist/analyze/astDetectors.js +566 -0
- package/dist/analyze/buildResourceFindings.d.ts +34 -0
- package/dist/analyze/buildResourceFindings.js +86 -0
- package/dist/analyze/cli.d.ts +20 -0
- package/dist/analyze/cli.js +186 -0
- package/dist/analyze/cpuResourceDetectors.d.ts +35 -0
- package/dist/analyze/cpuResourceDetectors.js +163 -0
- package/dist/analyze/heuristicScan.d.ts +61 -0
- package/dist/analyze/heuristicScan.js +91 -0
- package/dist/analyze/index.d.ts +15 -0
- package/dist/analyze/index.js +15 -0
- package/dist/analyze/loadTypescript.d.ts +8 -0
- package/dist/analyze/loadTypescript.js +23 -0
- package/dist/analyze/memoryIoDetectors.d.ts +15 -0
- package/dist/analyze/memoryIoDetectors.js +68 -0
- package/dist/analyze/preferAstDetectors.d.ts +18 -0
- package/dist/analyze/preferAstDetectors.js +91 -0
- package/dist/analyze/projectScan.d.ts +19 -0
- package/dist/analyze/projectScan.js +115 -0
- package/dist/analyze/resourceAdmission.d.ts +33 -0
- package/dist/analyze/resourceAdmission.js +91 -0
- package/dist/analyze/resourceFindingCandidate.d.ts +61 -0
- package/dist/analyze/resourceFindingCandidate.js +75 -0
- package/dist/analyze/retryAmplificationDetector.d.ts +11 -0
- package/dist/analyze/retryAmplificationDetector.js +55 -0
- package/dist/analyze/schemas/resource-finding.schema.json +352 -0
- package/dist/analyze/serviceDiscovery.d.ts +66 -0
- package/dist/analyze/serviceDiscovery.js +210 -0
- package/dist/analyze/unboundedParallelismDetector.d.ts +11 -0
- package/dist/analyze/unboundedParallelismDetector.js +57 -0
- package/dist/analyze/validateResourceFinding.d.ts +19 -0
- package/dist/analyze/validateResourceFinding.js +46 -0
- package/dist/bullmq.d.ts +33 -0
- package/dist/bullmq.js +106 -0
- package/dist/cli/helpText.d.ts +8 -0
- package/dist/cli/helpText.js +99 -0
- package/dist/cli/netgreener.d.ts +12 -0
- package/dist/cli/netgreener.js +65 -0
- package/dist/collectorIpc.d.ts +28 -0
- package/dist/collectorIpc.js +189 -0
- package/dist/collectorMetadata.d.ts +15 -0
- package/dist/collectorMetadata.js +33 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +97 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.js +1 -0
- package/dist/contract/observation-envelope.schema.json +331 -0
- package/dist/contract/observation-protobuf-view.schema.json +773 -0
- package/dist/contract/observation-v1.d.mts +42 -0
- package/dist/contract/observation-v1.mjs +555 -0
- package/dist/contract/observationIdentity.d.ts +11 -0
- package/dist/contract/observationIdentity.js +48 -0
- package/dist/contract/observationProjection.d.ts +3 -0
- package/dist/contract/observationProjection.js +29 -0
- package/dist/contract/validate.d.ts +20 -0
- package/dist/contract/validate.js +227 -0
- package/dist/dogfood/mp3WorkerGates.d.ts +54 -0
- package/dist/dogfood/mp3WorkerGates.js +86 -0
- package/dist/dogfood/retainDryRunArtifact.d.ts +45 -0
- package/dist/dogfood/retainDryRunArtifact.js +103 -0
- package/dist/dogfood/tenantDogfoodGates.d.ts +76 -0
- package/dist/dogfood/tenantDogfoodGates.js +206 -0
- package/dist/exporter.d.ts +56 -0
- package/dist/exporter.js +304 -0
- package/dist/express.d.ts +44 -0
- package/dist/express.js +118 -0
- package/dist/externalApiMeter.d.ts +72 -0
- package/dist/externalApiMeter.js +1167 -0
- package/dist/fastify.d.ts +53 -0
- package/dist/fastify.js +148 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +31 -0
- package/dist/n4/deploymentMatrix.d.ts +22 -0
- package/dist/n4/deploymentMatrix.js +57 -0
- package/dist/n4/missingnessInventory.d.ts +22 -0
- package/dist/n4/missingnessInventory.js +72 -0
- package/dist/nest.d.ts +42 -0
- package/dist/nest.js +87 -0
- package/dist/processResources.d.ts +28 -0
- package/dist/processResources.js +31 -0
- package/dist/processRuntime.d.ts +35 -0
- package/dist/processRuntime.js +96 -0
- package/dist/requestContext.d.ts +21 -0
- package/dist/requestContext.js +33 -0
- package/dist/runtime.d.ts +60 -0
- package/dist/runtime.js +289 -0
- package/dist/runtimeHealth.d.ts +35 -0
- package/dist/runtimeHealth.js +121 -0
- package/dist/serverlessHints.d.ts +26 -0
- package/dist/serverlessHints.js +52 -0
- package/dist/tenantContext.d.ts +78 -0
- package/dist/tenantContext.js +226 -0
- package/dist/tenantDefaults.d.ts +9 -0
- package/dist/tenantDefaults.js +9 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.js +2 -0
- package/dist/uploader.d.ts +23 -0
- package/dist/uploader.js +52 -0
- package/dist/windowId.d.ts +2 -0
- package/dist/windowId.js +7 -0
- package/examples/analyze-manifest.mjs +30 -0
- package/examples/bullmq-live-smoke.mjs +178 -0
- package/examples/bullmq-mp3-worker-gate.mjs +237 -0
- package/examples/express-dry-run.mjs +72 -0
- package/examples/process-mp3-restart-gate.mjs +135 -0
- package/examples/retain-dry-run.mjs +110 -0
- package/examples/tenant-dogfood.mjs +269 -0
- package/fixtures/analyze-sample/architectureNoise.ts +13 -0
- package/fixtures/analyze-sample/cpuHotPaths.ts +27 -0
- package/fixtures/analyze-sample/fanOutClient.ts +15 -0
- package/fixtures/analyze-sample/inferenceHotPaths.ts +22 -0
- package/fixtures/analyze-sample/memoryIoHotPaths.ts +21 -0
- package/fixtures/analyze-sample/modelLoadHotPaths.ts +11 -0
- package/fixtures/analyze-sample/nestedLookup.ts +15 -0
- package/fixtures/analyze-sample/retryClient.ts +10 -0
- package/fixtures/analyze-sample/securitySmell.ts +8 -0
- package/fixtures/analyze-sample/server.ts +11 -0
- package/fixtures/analyze-sample/styleOnly.ts +7 -0
- package/fixtures/analyze-sample/worker.ts +4 -0
- package/fixtures/dual-view-projection.v1.json +53 -0
- package/fixtures/identity-preimages.v1.json +42 -0
- package/fixtures/mp2_shadow_digest_golden.v1.json +47 -0
- package/fixtures/projection-batch-document.v1.json +170 -0
- package/fixtures/projection-batch.v1.json +9 -0
- package/fixtures/python_reference_shapes.md +24 -0
- package/fixtures/session_metadata_runtime_v0.json +120 -0
- package/fixtures/session_metadata_with_tenant.json +85 -0
- package/package.json +137 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog — @netgreener/runtime
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Express / Fastify / Nest-via-adapter middleware with tenant attribution
|
|
7
|
+
- Outbound `fetch` metering (`external_api_v0`), optional axios/undici wraps
|
|
8
|
+
- BullMQ processor wrap and process/cron helpers
|
|
9
|
+
- Default `NETGREENER_EXPORT_MODE=direct` upload to NetGreener API
|
|
10
|
+
- Opt-in collector IPC and thin-flush documentation in the tutorial
|
|
11
|
+
- Local `netgreener analyze` scaffold (`run` / `optimize` stubs)
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
- First public npm release ships with npm provenance from this repository
|
|
15
|
+
- Process CPU/RSS are measured on instrumented hooks; cgroup/process-tree
|
|
16
|
+
sampling is not claimed in this version
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @netgreener/runtime
|
|
2
|
+
|
|
3
|
+
NetGreener Node.js Runtime — thin adapter that meters Express / Fastify / Nest /
|
|
4
|
+
BullMQ / process workloads and uploads RunSessions to the NetGreener API.
|
|
5
|
+
|
|
6
|
+
## Docs
|
|
7
|
+
|
|
8
|
+
- **Tutorial:** [TUTORIAL.md](./TUTORIAL.md)
|
|
9
|
+
- **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
|
|
10
|
+
|
|
11
|
+
## Install (when published)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @netgreener/runtime
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Develop
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm ci
|
|
21
|
+
npm test
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
See package `license` field. Source of truth for internal engineering remains
|
|
27
|
+
on the private Azure DevOps repository; this GitHub repo is the public publish
|
|
28
|
+
mirror used for npm provenance.
|
package/TUTORIAL.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# Node service runtime tutorial — HTTP, workers, and scripts
|
|
2
|
+
|
|
3
|
+
**Twin of:** [`SERVICE_RUNTIME_TUTORIAL.md`](../docs/SERVICE_RUNTIME_TUTORIAL.md)
|
|
4
|
+
**§4–§6** (FastAPI / Celery / process wrap).
|
|
5
|
+
**Package:** `@netgreener/runtime` (private, experimental).
|
|
6
|
+
**Not claimed:** MP3 production support, npm publish, default collector cutover,
|
|
7
|
+
managed Node sidecar collector parity with Python Celery, or live acceptance
|
|
8
|
+
without your retained evidence.
|
|
9
|
+
|
|
10
|
+
| Need deeper reference | Doc |
|
|
11
|
+
|-----------------------|-----|
|
|
12
|
+
| Node backlog / gates | [`NODE_RUNTIME_PARITY_PLAN.md`](../docs/NODE_RUNTIME_PARITY_PLAN.md) |
|
|
13
|
+
| Worker staging gate | [`WORKER_STAGING_GATE.md`](WORKER_STAGING_GATE.md) |
|
|
14
|
+
| Package status / CI | [`README.md`](README.md), [`ROADMAP.md`](ROADMAP.md) |
|
|
15
|
+
| Tenant dogfood | [`NODE_TENANT_DOGFOOD.md`](../docs/NODE_TENANT_DOGFOOD.md) |
|
|
16
|
+
| Service tokens | [`CI_SERVICE_TOKEN_ONBOARDING.md`](../docs/CI_SERVICE_TOKEN_ONBOARDING.md) |
|
|
17
|
+
| Python twin | [`SERVICE_RUNTIME_TUTORIAL.md`](../docs/SERVICE_RUNTIME_TUTORIAL.md) |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 0. What you will run
|
|
22
|
+
|
|
23
|
+
| Project has… | Situation | NetGreener path | Code change? |
|
|
24
|
+
|--------------|-----------|-----------------|--------------|
|
|
25
|
+
| **Express** HTTP API | Per-**route** KPIs | `netgreenerExpressMiddleware` | **Yes — one middleware line** (§4) |
|
|
26
|
+
| **Fastify** HTTP API | Per-**route** KPIs | `netgreenerFastifyPlugin` | **Yes — one `register`** (§4) |
|
|
27
|
+
| **Nest** | Express or Fastify under the hood | `applyNetGreenerNestHooks(app)` | **Yes — one hook** (§4) |
|
|
28
|
+
| **BullMQ** worker | Per-**task** KPIs | `netgreenerBullMqProcessor` | **Yes — wrap the processor** (§5) |
|
|
29
|
+
| **Script / cron / batch** `node …` | Process window + outbound `fetch` | `startProcessRuntime` / `runWithProcessTenant` | **Yes — start helper** (§6) |
|
|
30
|
+
|
|
31
|
+
Default upload mode remains **`NETGREENER_EXPORT_MODE=direct`**. Sibling collector
|
|
32
|
+
IPC is opt-in only; do not cut over the default without explicit authorization.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
HTTP: middleware/plugin → window → POST /api/v1/runsessions/ (default direct; or dry-run)
|
|
36
|
+
BullMQ: wrapped processor → window → same upload path (v0; no Python-style managed sidecar yet)
|
|
37
|
+
Process: startProcessRuntime → window → same upload path
|
|
38
|
+
|
|
39
|
+
Opt-in: NETGREENER_EXPORT_MODE=collector → length-prefixed IPC → sibling collector spool
|
|
40
|
+
(default remains direct; see §4.5)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 1. Worksheet (your project)
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
App entry file: ________ (creates Express app or Fastify instance)
|
|
49
|
+
Framework: Express / Fastify / Nest→Express / Nest→Fastify
|
|
50
|
+
ORG_ID: ________
|
|
51
|
+
PROJECT_ID: ________
|
|
52
|
+
API URL: https://core-api.netgreener.com (or your host)
|
|
53
|
+
Token secret name: NETGREENER_TOKEN
|
|
54
|
+
Dry-run first?: yes (recommended)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Find **ORG_ID** / **PROJECT_ID** in the NetGreener dashboard (project URL looks like
|
|
58
|
+
`https://dashboard.netgreener.com/projects/<PROJECT_ID>`).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Get a token
|
|
63
|
+
|
|
64
|
+
**Automation / servers (recommended):**
|
|
65
|
+
|
|
66
|
+
1. Create an API / service token (`ngs_…`) in the dashboard.
|
|
67
|
+
2. Store it as secret `NETGREENER_TOKEN`.
|
|
68
|
+
3. Details: [`CI_SERVICE_TOKEN_ONBOARDING.md`](../docs/CI_SERVICE_TOKEN_ONBOARDING.md).
|
|
69
|
+
|
|
70
|
+
**Local dry-run only:** you can skip the token and set `NETGREENER_RUNTIME_DRY_RUN=1`
|
|
71
|
+
(see §4.4). Dry-run builds a real payload shape and does **not** upload.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 3. Install `@netgreener/runtime` in that project
|
|
76
|
+
|
|
77
|
+
The package is **private** today (Azure DevOps / internal registry). Install from your
|
|
78
|
+
approved feed or path — there is **no** public npm publish yet.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Example once the private feed is configured for your project:
|
|
82
|
+
npm install @netgreener/runtime
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Engines: Node **22** or **24** (see `package.json` `engines`).
|
|
86
|
+
|
|
87
|
+
Verify the import resolves:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { netgreenerExpressMiddleware } from '@netgreener/runtime'
|
|
91
|
+
console.log(typeof netgreenerExpressMiddleware)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 4. Express or Fastify — any Node HTTP API
|
|
97
|
+
|
|
98
|
+
Starting the app with `node`, `tsx`, PM2, or a container entrypoint does not change
|
|
99
|
+
the hook — those are process managers/servers.
|
|
100
|
+
|
|
101
|
+
### 4.1 Express — one middleware line
|
|
102
|
+
|
|
103
|
+
In the **same file** that creates `const app = express()` (not only in routers):
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import express from 'express'
|
|
107
|
+
import { netgreenerExpressMiddleware, getExpressRuntime } from '@netgreener/runtime'
|
|
108
|
+
|
|
109
|
+
const app = express()
|
|
110
|
+
app.use(netgreenerExpressMiddleware())
|
|
111
|
+
|
|
112
|
+
app.get('/health', (_req, res) => res.send('ok'))
|
|
113
|
+
// ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Optional manual flush (tests / graceful shutdown):
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
await getExpressRuntime().flush('manual')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4.2 Fastify — one plugin register
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import Fastify from 'fastify'
|
|
126
|
+
import {
|
|
127
|
+
netgreenerFastifyPlugin,
|
|
128
|
+
getFastifyRuntime,
|
|
129
|
+
bindFastifyTenant,
|
|
130
|
+
} from '@netgreener/runtime'
|
|
131
|
+
|
|
132
|
+
const app = Fastify()
|
|
133
|
+
await app.register(netgreenerFastifyPlugin())
|
|
134
|
+
|
|
135
|
+
app.get('/health', async () => 'ok')
|
|
136
|
+
|
|
137
|
+
app.get('/me', async (request) => {
|
|
138
|
+
// After auth: prefer bindFastifyTenant (request-scoped store).
|
|
139
|
+
bindFastifyTenant(request, {
|
|
140
|
+
tenantId: 'org_acme',
|
|
141
|
+
tenantSource: 'verified-auth',
|
|
142
|
+
})
|
|
143
|
+
return { ok: true }
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Notes:
|
|
148
|
+
|
|
149
|
+
- The plugin breaks Fastify encapsulation (`skip-override`) so hooks apply to routes
|
|
150
|
+
registered on the same instance after `register`.
|
|
151
|
+
- Prefixed routes appear as `/prefix/path/:id` in `service_unit` (Fastify template).
|
|
152
|
+
- Raw request URLs are never metering fallbacks (`<unmatched>` when no template).
|
|
153
|
+
|
|
154
|
+
### 4.2b Nest — via underlying Express or Fastify adapter
|
|
155
|
+
|
|
156
|
+
No separate Nest interception layer. After `NestFactory.create`:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { NestFactory } from '@nestjs/core'
|
|
160
|
+
import { applyNetGreenerNestHooks } from '@netgreener/runtime'
|
|
161
|
+
import { AppModule } from './app.module.js'
|
|
162
|
+
|
|
163
|
+
const app = await NestFactory.create(AppModule)
|
|
164
|
+
await applyNetGreenerNestHooks(app)
|
|
165
|
+
await app.listen(3000)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or explicitly:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { netgreenerNestExpressMiddleware } from '@netgreener/runtime'
|
|
172
|
+
// Express adapter:
|
|
173
|
+
app.use(netgreenerNestExpressMiddleware())
|
|
174
|
+
|
|
175
|
+
// Fastify adapter:
|
|
176
|
+
// await app.getHttpAdapter().getInstance().register(netgreenerNestFastifyPlugin())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 4.3 Env for the API process
|
|
180
|
+
|
|
181
|
+
**Dry-run (no upload):**
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
NETGREENER_SERVICE_RUNTIME=1
|
|
185
|
+
NETGREENER_RUNTIME_DRY_RUN=1
|
|
186
|
+
NETGREENER_TOKEN=ngs_placeholder
|
|
187
|
+
NETGREENER_PROJECT_ID=1
|
|
188
|
+
# optional tenant header binding:
|
|
189
|
+
# NETGREENER_TENANT_SOURCE=header
|
|
190
|
+
# NETGREENER_TENANT_HEADER=X-Organization-Id
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Live upload (only with a real token; you retain evidence):**
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
NETGREENER_SERVICE_RUNTIME=1
|
|
197
|
+
NETGREENER_API_URL=https://core-api.netgreener.com
|
|
198
|
+
NETGREENER_TOKEN=<token>
|
|
199
|
+
NETGREENER_ORG_ID=<org>
|
|
200
|
+
NETGREENER_PROJECT_ID=<project>
|
|
201
|
+
NETGREENER_DEPLOY_ENVIRONMENT=staging
|
|
202
|
+
# Leave NETGREENER_RUNTIME_DRY_RUN unset or 0
|
|
203
|
+
# Leave NETGREENER_EXPORT_MODE unset (default direct) unless authorized for collector
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 4.4 Prove it
|
|
207
|
+
|
|
208
|
+
**A. Dry-run / CI smoke (no api_server):**
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cd path/to/netgreener_node # or your app with the package linked
|
|
212
|
+
npm test
|
|
213
|
+
NETGREENER_RUNTIME_DRY_RUN=1 npm run example:dogfood-tenant
|
|
214
|
+
npm run example:dry-run
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Pass = scripts exit 0; payloads validate; no live upload required.
|
|
218
|
+
|
|
219
|
+
**B. Live (authorized):**
|
|
220
|
+
|
|
221
|
+
1. Deploy / start the API with live env (§4.3).
|
|
222
|
+
2. Call 2–3 real routes (health + one business route).
|
|
223
|
+
3. Stop the process (or wait for `NETGREENER_RUNTIME_FLUSH_MINUTES`, default 5).
|
|
224
|
+
4. Dashboard → **Project `<PROJECT_ID>`** → newest **Run** → **Runtime Ops** / API Runtime.
|
|
225
|
+
5. Pass = bounded route templates listed (e.g. `GET /health`), not raw customer paths.
|
|
226
|
+
6. Keep a sanitized evidence record (revision, env, run id) — see
|
|
227
|
+
[`NODE_TENANT_DOGFOOD.md`](../docs/NODE_TENANT_DOGFOOD.md).
|
|
228
|
+
|
|
229
|
+
Live failure (non-2xx / missing token) must **fail** the run; local payload capture is
|
|
230
|
+
not a substitute for a successful upload.
|
|
231
|
+
|
|
232
|
+
### 4.5 Opt-in sibling collector (HTTP → MP2 IPC)
|
|
233
|
+
|
|
234
|
+
Default remains **`NETGREENER_EXPORT_MODE=direct`** (adapter POSTs RunSession). To send
|
|
235
|
+
HTTP window flushes to a sibling collector instead:
|
|
236
|
+
|
|
237
|
+
1. Start the collector (see
|
|
238
|
+
[`COLLECTOR_SIBLING.md`](../netgreener_contracts/planning/mp2-collector/COLLECTOR_SIBLING.md)).
|
|
239
|
+
2. In the **API process** (same middleware as §4.1 / §4.2):
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
NETGREENER_EXPORT_MODE=collector
|
|
243
|
+
NETGREENER_COLLECTOR_ENDPOINT=tcp://127.0.0.1:17999
|
|
244
|
+
# Leave NETGREENER_RUNTIME_DRY_RUN unset — dry-run skips collector IPC on purpose
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
3. Hit routes and flush as in §4.4. Successful path = durable spool ACK over length-prefixed
|
|
248
|
+
IPC (`run_session_window`), not a silent fall-back to direct upload.
|
|
249
|
+
|
|
250
|
+
**Honesty:** this proves the Node HTTP → exporter → collector wire (Express, Fastify,
|
|
251
|
+
and Nest-via-Express / Nest-via-Fastify adapters). It does **not** flip the default,
|
|
252
|
+
claim MP2 exit, Observation cloud ingest, or Python-style managed sidecar.
|
|
253
|
+
Worker/process twins: §5.5 / §6.1. Local proofs:
|
|
254
|
+
`express.collector.integration.test.ts`,
|
|
255
|
+
`fastify.collector.integration.test.ts`,
|
|
256
|
+
`nest.collector.integration.test.ts`,
|
|
257
|
+
`nest.fastify.collector.integration.test.ts`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 5. BullMQ — any worker project (Part 5 twin)
|
|
262
|
+
|
|
263
|
+
Unlike Python Celery (entrypoint wrap, often **no** task code changes), Node BullMQ
|
|
264
|
+
v0 meters by **wrapping the processor** you already pass to `Worker`. There is **no**
|
|
265
|
+
`netgreener runtime entrypoint` CLI for Node yet, and **no** `NETGREENER_COLLECTOR_MODE=managed`
|
|
266
|
+
sidecar requirement for this path — default remains direct flush from the worker process.
|
|
267
|
+
|
|
268
|
+
### 5.1 Wrap the processor (required for task KPIs)
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
import { Worker } from 'bullmq'
|
|
272
|
+
import { netgreenerBullMqProcessor, getBullMqRuntime } from '@netgreener/runtime'
|
|
273
|
+
|
|
274
|
+
const processor = netgreenerBullMqProcessor(async (job) => {
|
|
275
|
+
// your existing job logic
|
|
276
|
+
await fetch('https://api.openai.com/v1/chat/completions', { /* … */ })
|
|
277
|
+
return { ok: true }
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const worker = new Worker('queue-name', processor, { connection: { /* redis */ } })
|
|
281
|
+
|
|
282
|
+
// optional: await getBullMqRuntime().flush('manual')
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Each job records a unit like `task:<job.name>`. Outbound `fetch` / axios made inside
|
|
286
|
+
the processor inherit that task unit when ALS is active.
|
|
287
|
+
|
|
288
|
+
### 5.2 Tenant from job data
|
|
289
|
+
|
|
290
|
+
Set env (or programmatic config) so tenant is read from `job.data`:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
NETGREENER_TENANT_SOURCE=task_kwarg
|
|
294
|
+
NETGREENER_TENANT_TASK_KWARG=organization_id # default
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Enqueue with the kwarg present:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
await queue.add('ocr', { organization_id: 'org_acme', /* … */ })
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 5.3 Env for the worker process
|
|
304
|
+
|
|
305
|
+
Same token/project vars as §4.3. Prefer dry-run first:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
NETGREENER_SERVICE_RUNTIME=1
|
|
309
|
+
NETGREENER_RUNTIME_DRY_RUN=1
|
|
310
|
+
NETGREENER_TENANT_SOURCE=task_kwarg
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 5.4 Prove it
|
|
314
|
+
|
|
315
|
+
1. Start the worker with the wrapped processor.
|
|
316
|
+
2. Enqueue **one** job the way your project already does.
|
|
317
|
+
3. Flush (shutdown or `getBullMqRuntime().flush('manual')`).
|
|
318
|
+
4. Dry-run: payload includes `task:<name>` and optional `by_tenant`.
|
|
319
|
+
5. Live (authorized): dashboard Runtime Ops shows the task row.
|
|
320
|
+
|
|
321
|
+
**Honesty gap vs Python Part 5:** managed collector / process-tree sidecar for Node
|
|
322
|
+
workers is still open under MP2–MP3. Real Redis + `bullmq` Worker smoke exists
|
|
323
|
+
(`examples/bullmq-live-smoke.mjs`; live run **39574**) — that is not managed-sidecar
|
|
324
|
+
parity.
|
|
325
|
+
|
|
326
|
+
### 5.5 Opt-in sibling collector (BullMQ → MP2 IPC)
|
|
327
|
+
|
|
328
|
+
Same env as §4.5 on the **worker process** (default remains `direct`):
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
NETGREENER_EXPORT_MODE=collector
|
|
332
|
+
NETGREENER_COLLECTOR_ENDPOINT=tcp://127.0.0.1:17999
|
|
333
|
+
# Leave NETGREENER_RUNTIME_DRY_RUN unset — dry-run skips collector IPC
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Successful flush = `run_session_window` with `collector: bullmq_worker` and
|
|
337
|
+
`task:<job.name>` units over IPC — **not** a Python-style managed sidecar and **not**
|
|
338
|
+
a silent fall-back to direct upload. Local proof:
|
|
339
|
+
`src/tests/workers.collector.integration.test.ts`.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 6. Scripts, cron, sidecars, batch jobs (Part 6 twin)
|
|
344
|
+
|
|
345
|
+
Use this when the work is **not** an HTTP API and **not** a BullMQ worker:
|
|
346
|
+
nightly jobs, ETL, report generators, simple long-lived Node processes.
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
import {
|
|
350
|
+
startProcessRuntime,
|
|
351
|
+
runWithProcessTenant,
|
|
352
|
+
flushProcessRuntime,
|
|
353
|
+
} from '@netgreener/runtime'
|
|
354
|
+
|
|
355
|
+
startProcessRuntime()
|
|
356
|
+
|
|
357
|
+
await runWithProcessTenant('org_acme', async () => {
|
|
358
|
+
// batch work + outbound fetch…
|
|
359
|
+
}, { serviceUnit: 'task:nightly_report' })
|
|
360
|
+
|
|
361
|
+
await flushProcessRuntime('manual')
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Env: same as §4.3 (`NETGREENER_SERVICE_RUNTIME=1`, dry-run or live token). Leave
|
|
365
|
+
export mode at default `direct` unless authorized for collector.
|
|
366
|
+
|
|
367
|
+
You get a **process/task** window and supported outbound HTTP observation — **not**
|
|
368
|
+
Express/Fastify per-route rows and **not** BullMQ `task:…` rows unless you also use §5.
|
|
369
|
+
|
|
370
|
+
There is no `netgreener runtime process -- node job.js` CLI wrapper in this package
|
|
371
|
+
yet; call `startProcessRuntime` from your entry file (or a thin launcher you own).
|
|
372
|
+
|
|
373
|
+
### 6.1 Opt-in sibling collector (process → MP2 IPC)
|
|
374
|
+
|
|
375
|
+
Same env as §4.5 / §5.5. Flush via `flushProcessRuntime('manual')` or shutdown.
|
|
376
|
+
IPC payload uses `collector: node_process`. Still **not** a managed process-tree
|
|
377
|
+
sampler or CLI launcher — those remain MP3. Proof lives in
|
|
378
|
+
`src/tests/workers.collector.integration.test.ts`.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 7. Common failures
|
|
383
|
+
|
|
384
|
+
| Symptom | Check |
|
|
385
|
+
|---------|--------|
|
|
386
|
+
| No rows in dashboard | `NETGREENER_SERVICE_RUNTIME=1`, real token, dry-run unset for live |
|
|
387
|
+
| Only dry-run locally | Expected when `NETGREENER_RUNTIME_DRY_RUN=1` |
|
|
388
|
+
| Raw paths / IDs in units | Bug — should be templates or `<unmatched>`; file an issue |
|
|
389
|
+
| Fastify routes not metered | Ensure `register(netgreenerFastifyPlugin())` before routes; package includes skip-override |
|
|
390
|
+
| Tenant missing after auth (Fastify) | Use `bindFastifyTenant(request, …)` |
|
|
391
|
+
| BullMQ jobs invisible | Processor must be wrapped with `netgreenerBullMqProcessor` |
|
|
392
|
+
| BullMQ tenant missing | `NETGREENER_TENANT_SOURCE=task_kwarg` + kwarg in `job.data` |
|
|
393
|
+
| Package not found on npm | Expected — package is private until MP3 publish |
|
|
394
|
+
| Serverless / Lambda freezes mid-window | Use thin-flush (§9); flush at invoke end; durability is best-effort |
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## 8. Minimal copy-paste canaries
|
|
399
|
+
|
|
400
|
+
**Express dry-run:** middleware + `/health` + `NETGREENER_RUNTIME_DRY_RUN=1` +
|
|
401
|
+
`getExpressRuntime().flush('manual')`.
|
|
402
|
+
|
|
403
|
+
**Fastify dry-run:** `register(plugin)` + `/health` + dry-run env +
|
|
404
|
+
`getFastifyRuntime().flush('manual')`.
|
|
405
|
+
|
|
406
|
+
**BullMQ dry-run:** wrap processor + one fake job + `getBullMqRuntime().flush('manual')`
|
|
407
|
+
(see `examples/tenant-dogfood.mjs`).
|
|
408
|
+
|
|
409
|
+
**Process dry-run:** `startProcessRuntime` + `runWithProcessTenant` + flush.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 9. Thin-flush / serverless mode
|
|
414
|
+
|
|
415
|
+
Long-running VM / container / K8s Node should keep default **`direct`** or opt-in
|
|
416
|
+
**`collector`** (§4.5). Serverless hosts often cannot run a durable sibling collector.
|
|
417
|
+
|
|
418
|
+
### When to use thin
|
|
419
|
+
|
|
420
|
+
Set **one** of:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
NETGREENER_EXPORT_MODE=thin
|
|
424
|
+
# or alias when EXPORT_MODE is unset:
|
|
425
|
+
NETGREENER_FLUSH_MODE=thin
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Optional shorter window:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
NETGREENER_RUNTIME_FLUSH_MINUTES=1
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### What thin does (honesty)
|
|
435
|
+
|
|
436
|
+
| Behavior | Thin mode |
|
|
437
|
+
|----------|-----------|
|
|
438
|
+
| Upload path | Same direct RunSession POST as `direct` today |
|
|
439
|
+
| Durability grade | `best_effort_bounded` (visible; not silent drop) |
|
|
440
|
+
| Sibling collector spool | **No** |
|
|
441
|
+
| Auto-detect platforms | **No** — `detectServerlessPlatformSignals()` is informational only |
|
|
442
|
+
|
|
443
|
+
Platform signals the helper recognizes (for your own wiring / checks):
|
|
444
|
+
`AWS_LAMBDA_FUNCTION_NAME`, `FUNCTIONS_WORKER_RUNTIME` / `AZURE_FUNCTIONS_ENVIRONMENT`,
|
|
445
|
+
`FUNCTION_TARGET` / `FUNCTION_SIGNATURE_TYPE`, `VERCEL` / `VERCEL_ENV`, `NETLIFY`.
|
|
446
|
+
|
|
447
|
+
```js
|
|
448
|
+
import {
|
|
449
|
+
detectServerlessPlatformSignals,
|
|
450
|
+
exportModeFromEnv,
|
|
451
|
+
} from '@netgreener/runtime'
|
|
452
|
+
|
|
453
|
+
const hint = detectServerlessPlatformSignals()
|
|
454
|
+
// hint.suggestedExportMode === 'thin' when a host signal is present — still not applied.
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Invoke-end flush (required for thin)
|
|
458
|
+
|
|
459
|
+
Call flush before the isolate freezes:
|
|
460
|
+
|
|
461
|
+
```js
|
|
462
|
+
import { getExpressRuntime } from '@netgreener/runtime'
|
|
463
|
+
|
|
464
|
+
export async function handler(event, context) {
|
|
465
|
+
try {
|
|
466
|
+
// … handle request with middleware-wrapped app …
|
|
467
|
+
} finally {
|
|
468
|
+
await getExpressRuntime().flush('shutdown')
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Not claimed:** Lambda extensions, automatic freeze hooks, MP3 serverless support, or
|
|
474
|
+
flipping the default off `direct` for long-running Node.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 10. Stop here (honesty)
|
|
479
|
+
|
|
480
|
+
This tutorial does **not** by itself:
|
|
481
|
+
- publish `@netgreener/runtime` to npm
|
|
482
|
+
- establish MP3 support, Python-equivalent managed worker collectors, npm release, or
|
|
483
|
+
live acceptance without your retained evidence
|
|
484
|
+
- flip `NETGREENER_EXPORT_MODE` default away from `direct`
|
|
485
|
+
|
|
486
|
+
Package examples: `examples/express-dry-run.mjs`, `examples/tenant-dogfood.mjs`.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Revision note
|
|
491
|
+
|
|
492
|
+
This tutorial documents **experimental** Node HTTP (N1), Nest-via-adapter, BullMQ,
|
|
493
|
+
and process helpers (N3) as of the stacked Node tutorial slices. It does **not**
|
|
494
|
+
establish MP3 support, Python-equivalent managed worker collectors, npm release, or
|
|
495
|
+
default export-mode cutover.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ServiceRuntimeV0, TenantId } from './types.js';
|
|
2
|
+
export declare class ServiceRuntimeAggregator {
|
|
3
|
+
private units;
|
|
4
|
+
private byTenant;
|
|
5
|
+
private windowStartedAt;
|
|
6
|
+
resetWindow(): void;
|
|
7
|
+
windowStartedMs(): number;
|
|
8
|
+
record(sample: {
|
|
9
|
+
serviceUnit: string;
|
|
10
|
+
unitType?: string;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
error?: boolean;
|
|
13
|
+
tenantId?: TenantId | null;
|
|
14
|
+
/** Measured process CPU ms for this sample (CAP-R4); omit → duration proxy. */
|
|
15
|
+
cpuTimeMs?: number;
|
|
16
|
+
/** Measured RSS KiB at sample end. */
|
|
17
|
+
peakRssKb?: number;
|
|
18
|
+
}): void;
|
|
19
|
+
private recordIntoMap;
|
|
20
|
+
private buildUnitsList;
|
|
21
|
+
hasData(): boolean;
|
|
22
|
+
totalDurationMs(): number;
|
|
23
|
+
/** Sum of measured process CPU ms in this window (0 if none measured). */
|
|
24
|
+
totalMeasuredCpuMs(): number;
|
|
25
|
+
/** True when any unit recorded measured process CPU (not duration proxy). */
|
|
26
|
+
hasMeasuredCpu(): boolean;
|
|
27
|
+
/** True when any unit recorded peak RSS. */
|
|
28
|
+
hasMeasuredRss(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Build service_runtime_v0. N1 uses duration-share energy attribution (trend).
|
|
31
|
+
* CPU-accurate share can land later without changing the schema.
|
|
32
|
+
*/
|
|
33
|
+
buildV0(opts: {
|
|
34
|
+
collector: string;
|
|
35
|
+
framework: string;
|
|
36
|
+
windowEnergyKwh: number;
|
|
37
|
+
windowSeconds: number;
|
|
38
|
+
}): ServiceRuntimeV0 | null;
|
|
39
|
+
/** Take a snapshot and reset live window (Python take_snapshot analogue). */
|
|
40
|
+
takeSnapshot(): ServiceRuntimeAggregator;
|
|
41
|
+
mergeSnapshot(snapshot: ServiceRuntimeAggregator): void;
|
|
42
|
+
private mergeMap;
|
|
43
|
+
}
|