@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process / cron / script runtime (N3) — no HTTP middleware required.
|
|
3
|
+
*
|
|
4
|
+
* Start metering, bind tenant for a job, flush on shutdown.
|
|
5
|
+
*/
|
|
6
|
+
import { type NetGreenerRuntime, type NetGreenerRuntimeOptions } from './runtime.js';
|
|
7
|
+
import type { UploadResult } from './uploader.js';
|
|
8
|
+
import { type TenantContext } from './tenantContext.js';
|
|
9
|
+
export type ProcessRuntimeOptions = NetGreenerRuntimeOptions & {
|
|
10
|
+
/** Default service unit when recording a named job (e.g. scheduled_report). */
|
|
11
|
+
serviceUnit?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Enable NetGreener runtime for a long-lived Node process (cron worker, script).
|
|
15
|
+
* Installs outbound ``fetch`` metering and periodic flush when config is ready.
|
|
16
|
+
*/
|
|
17
|
+
export declare function startProcessRuntime(opts?: ProcessRuntimeOptions): NetGreenerRuntime;
|
|
18
|
+
/**
|
|
19
|
+
* Run ``fn`` with an explicit tenant id (batch/cron style — no JWT).
|
|
20
|
+
*/
|
|
21
|
+
export declare function runWithProcessTenant<T>(tenantId: string, fn: () => Promise<T> | T, opts?: {
|
|
22
|
+
source?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
serviceUnit?: string;
|
|
25
|
+
runtime?: NetGreenerRuntime;
|
|
26
|
+
}): Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Convenience: set tenant for the rest of the current async chain (enterWith).
|
|
29
|
+
* Prefer ``runWithProcessTenant`` when possible.
|
|
30
|
+
*/
|
|
31
|
+
export declare function bindProcessTenant(tenantId: string, opts?: {
|
|
32
|
+
source?: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
}): TenantContext;
|
|
35
|
+
export declare function flushProcessRuntime(kind?: 'manual' | 'shutdown'): Promise<UploadResult | null>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process / cron / script runtime (N3) — no HTTP middleware required.
|
|
3
|
+
*
|
|
4
|
+
* Start metering, bind tenant for a job, flush on shutdown.
|
|
5
|
+
*/
|
|
6
|
+
import { getRuntime, } from './runtime.js';
|
|
7
|
+
import { normalizeTenantId, runWithTenantContext, setTenantContext, } from './tenantContext.js';
|
|
8
|
+
import { runWithRequestAttribution } from './requestContext.js';
|
|
9
|
+
import { beginProcessResourceSample, endProcessResourceSample, } from './processResources.js';
|
|
10
|
+
let signalsInstalled = false;
|
|
11
|
+
/**
|
|
12
|
+
* Enable NetGreener runtime for a long-lived Node process (cron worker, script).
|
|
13
|
+
* Installs outbound ``fetch`` metering and periodic flush when config is ready.
|
|
14
|
+
*/
|
|
15
|
+
export function startProcessRuntime(opts = {}) {
|
|
16
|
+
const runtime = getRuntime({
|
|
17
|
+
collector: 'node_process',
|
|
18
|
+
framework: 'node',
|
|
19
|
+
...opts,
|
|
20
|
+
});
|
|
21
|
+
runtime.start();
|
|
22
|
+
if (!signalsInstalled) {
|
|
23
|
+
signalsInstalled = true;
|
|
24
|
+
const shutdown = () => {
|
|
25
|
+
void runtime.shutdown();
|
|
26
|
+
};
|
|
27
|
+
process.once('beforeExit', shutdown);
|
|
28
|
+
process.once('SIGINT', shutdown);
|
|
29
|
+
process.once('SIGTERM', shutdown);
|
|
30
|
+
}
|
|
31
|
+
return runtime;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run ``fn`` with an explicit tenant id (batch/cron style — no JWT).
|
|
35
|
+
*/
|
|
36
|
+
export async function runWithProcessTenant(tenantId, fn, opts) {
|
|
37
|
+
const normalized = normalizeTenantId(tenantId);
|
|
38
|
+
if (!normalized) {
|
|
39
|
+
throw new Error('invalid tenant_id');
|
|
40
|
+
}
|
|
41
|
+
const runtime = opts?.runtime ?? startProcessRuntime();
|
|
42
|
+
const ctx = {
|
|
43
|
+
tenantId: normalized,
|
|
44
|
+
tenantSource: opts?.source || 'manual',
|
|
45
|
+
...(opts?.label ? { tenantLabel: opts.label.slice(0, 128) } : {}),
|
|
46
|
+
};
|
|
47
|
+
const serviceUnit = opts?.serviceUnit || 'task:process';
|
|
48
|
+
const started = performance.now();
|
|
49
|
+
const resourceMark = beginProcessResourceSample();
|
|
50
|
+
let error = false;
|
|
51
|
+
const run = async () => {
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
error = true;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (runtime.enabled) {
|
|
61
|
+
const resources = endProcessResourceSample(resourceMark);
|
|
62
|
+
runtime.recordTask({
|
|
63
|
+
serviceUnit,
|
|
64
|
+
durationMs: performance.now() - started,
|
|
65
|
+
error,
|
|
66
|
+
tenantId: normalized,
|
|
67
|
+
cpuTimeMs: resources.cpuTimeMs,
|
|
68
|
+
peakRssKb: resources.rssKb,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return runWithTenantContext(ctx, () => runWithRequestAttribution({ serviceUnit }, () => run()));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convenience: set tenant for the rest of the current async chain (enterWith).
|
|
77
|
+
* Prefer ``runWithProcessTenant`` when possible.
|
|
78
|
+
*/
|
|
79
|
+
export function bindProcessTenant(tenantId, opts) {
|
|
80
|
+
const normalized = normalizeTenantId(tenantId);
|
|
81
|
+
if (!normalized)
|
|
82
|
+
throw new Error('invalid tenant_id');
|
|
83
|
+
const ctx = {
|
|
84
|
+
tenantId: normalized,
|
|
85
|
+
tenantSource: opts?.source || 'manual',
|
|
86
|
+
...(opts?.label ? { tenantLabel: opts.label.slice(0, 128) } : {}),
|
|
87
|
+
};
|
|
88
|
+
setTenantContext(ctx);
|
|
89
|
+
return ctx;
|
|
90
|
+
}
|
|
91
|
+
export async function flushProcessRuntime(kind = 'manual') {
|
|
92
|
+
const runtime = getRuntime({ collector: 'node_process', framework: 'node' });
|
|
93
|
+
if (kind === 'shutdown')
|
|
94
|
+
return runtime.shutdown();
|
|
95
|
+
return runtime.flush(kind);
|
|
96
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current inbound HTTP / task unit for outbound attribution.
|
|
3
|
+
*
|
|
4
|
+
* Prefer ``resolveServiceUnit`` when route metadata may appear after middleware
|
|
5
|
+
* entry (Express). Callers that await work (outbound fetch) must snapshot
|
|
6
|
+
* ``attributionServiceUnit()`` when the call begins. Static ``serviceUnit``
|
|
7
|
+
* remains for workers/batch.
|
|
8
|
+
*/
|
|
9
|
+
export type RequestAttribution = {
|
|
10
|
+
serviceUnit?: string;
|
|
11
|
+
resolveServiceUnit?: () => string;
|
|
12
|
+
};
|
|
13
|
+
export declare function runWithRequestAttribution<T>(attribution: RequestAttribution | null, fn: () => T): T;
|
|
14
|
+
/**
|
|
15
|
+
* Bind attribution for frameworks (e.g. Fastify) whose hooks return before the
|
|
16
|
+
* route runs — ``run()`` would exit too early. Cleared implicitly by the next
|
|
17
|
+
* ``run``/``enterWith`` on another request.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enterRequestAttribution(attribution: RequestAttribution | null): void;
|
|
20
|
+
export declare function getRequestAttribution(): RequestAttribution | null;
|
|
21
|
+
export declare function attributionServiceUnit(): string | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const storage = new AsyncLocalStorage();
|
|
3
|
+
export function runWithRequestAttribution(attribution, fn) {
|
|
4
|
+
return storage.run(attribution, fn);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Bind attribution for frameworks (e.g. Fastify) whose hooks return before the
|
|
8
|
+
* route runs — ``run()`` would exit too early. Cleared implicitly by the next
|
|
9
|
+
* ``run``/``enterWith`` on another request.
|
|
10
|
+
*/
|
|
11
|
+
export function enterRequestAttribution(attribution) {
|
|
12
|
+
storage.enterWith(attribution);
|
|
13
|
+
}
|
|
14
|
+
export function getRequestAttribution() {
|
|
15
|
+
return storage.getStore() ?? null;
|
|
16
|
+
}
|
|
17
|
+
export function attributionServiceUnit() {
|
|
18
|
+
const store = getRequestAttribution();
|
|
19
|
+
if (!store)
|
|
20
|
+
return null;
|
|
21
|
+
if (typeof store.resolveServiceUnit === 'function') {
|
|
22
|
+
try {
|
|
23
|
+
const resolved = store.resolveServiceUnit();
|
|
24
|
+
if (typeof resolved === 'string' && resolved.trim())
|
|
25
|
+
return resolved.trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fall through to static unit.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const staticUnit = (store.serviceUnit || '').trim();
|
|
32
|
+
return staticUnit || null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RuntimeConfig } from './config.js';
|
|
2
|
+
import { type RunSessionCreatePayload, type UploadResult } from './uploader.js';
|
|
3
|
+
import { type ObservationExporter } from './exporter.js';
|
|
4
|
+
export type FlushKind = 'periodic' | 'shutdown' | 'manual';
|
|
5
|
+
export type NetGreenerRuntimeOptions = {
|
|
6
|
+
config?: RuntimeConfig;
|
|
7
|
+
collector?: string;
|
|
8
|
+
framework?: string;
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
/** MP2: defaults to direct upload exporter (unchanged pipeline). */
|
|
11
|
+
exporter?: ObservationExporter;
|
|
12
|
+
onFlush?: (result: UploadResult, payload: RunSessionCreatePayload) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Process-wide runtime controller: record samples, periodic flush, shutdown flush.
|
|
16
|
+
*/
|
|
17
|
+
export declare class NetGreenerRuntime {
|
|
18
|
+
readonly config: RuntimeConfig;
|
|
19
|
+
private readonly aggregator;
|
|
20
|
+
private readonly collector;
|
|
21
|
+
private readonly framework;
|
|
22
|
+
private readonly fetchImpl;
|
|
23
|
+
private readonly exporter;
|
|
24
|
+
private readonly onFlush?;
|
|
25
|
+
private timer;
|
|
26
|
+
private flushing;
|
|
27
|
+
private started;
|
|
28
|
+
private outboundInstalled;
|
|
29
|
+
constructor(opts?: NetGreenerRuntimeOptions);
|
|
30
|
+
get enabled(): boolean;
|
|
31
|
+
private ensureOutboundMeter;
|
|
32
|
+
/** Prefer unwrapped fetch so telemetry uploads are never metered. */
|
|
33
|
+
private uploadFetch;
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
recordHttp(sample: {
|
|
37
|
+
method: string;
|
|
38
|
+
path: string;
|
|
39
|
+
statusCode: number;
|
|
40
|
+
durationMs: number;
|
|
41
|
+
/** Measured process CPU ms (from begin/endProcessResourceSample). */
|
|
42
|
+
cpuTimeMs?: number;
|
|
43
|
+
/** Measured RSS KiB at request end. */
|
|
44
|
+
peakRssKb?: number;
|
|
45
|
+
}): void;
|
|
46
|
+
/** Record a background job / worker task sample (BullMQ, cron, scripts). */
|
|
47
|
+
recordTask(sample: {
|
|
48
|
+
serviceUnit: string;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
error?: boolean;
|
|
51
|
+
tenantId?: string | null;
|
|
52
|
+
cpuTimeMs?: number;
|
|
53
|
+
peakRssKb?: number;
|
|
54
|
+
}): void;
|
|
55
|
+
flush(kind?: FlushKind): Promise<UploadResult | null>;
|
|
56
|
+
shutdown(): Promise<UploadResult | null>;
|
|
57
|
+
}
|
|
58
|
+
export declare function getRuntime(opts?: NetGreenerRuntimeOptions): NetGreenerRuntime;
|
|
59
|
+
/** Test helper — reset singleton. */
|
|
60
|
+
export declare function _resetRuntimeForTests(): void;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { ServiceRuntimeAggregator } from './aggregator.js';
|
|
3
|
+
import { configReady, loadRuntimeConfig, } from './config.js';
|
|
4
|
+
import { resolveObservationExporter, } from './exporter.js';
|
|
5
|
+
import { newRuntimeWindowId } from './windowId.js';
|
|
6
|
+
import { attachWindowTenantToRunContext, getTenantContext, tenantConfigFromRuntime, } from './tenantContext.js';
|
|
7
|
+
import { getExternalAggregator, getOriginalFetch, installOutboundInstrumentation, } from './externalApiMeter.js';
|
|
8
|
+
import { CPU_SOURCE_PROCESS, MEMORY_SOURCE_PROCESS, } from './processResources.js';
|
|
9
|
+
import { CPU_SOURCE_DURATION_PROXY } from './n4/missingnessInventory.js';
|
|
10
|
+
import { buildRuntimeHealthV0, markHooksActive, mergeHealthIntoMetadata, noteExportAttempt, noteExportMode, noteExportResult, noteFlushSkippedInFlight, _resetRuntimeHealthForTests, } from './runtimeHealth.js';
|
|
11
|
+
/**
|
|
12
|
+
* Process-wide runtime controller: record samples, periodic flush, shutdown flush.
|
|
13
|
+
*/
|
|
14
|
+
export class NetGreenerRuntime {
|
|
15
|
+
config;
|
|
16
|
+
aggregator = new ServiceRuntimeAggregator();
|
|
17
|
+
collector;
|
|
18
|
+
framework;
|
|
19
|
+
fetchImpl;
|
|
20
|
+
exporter;
|
|
21
|
+
onFlush;
|
|
22
|
+
timer = null;
|
|
23
|
+
flushing = false;
|
|
24
|
+
started = false;
|
|
25
|
+
outboundInstalled = false;
|
|
26
|
+
constructor(opts = {}) {
|
|
27
|
+
this.config = opts.config ?? loadRuntimeConfig();
|
|
28
|
+
this.collector = opts.collector ?? 'express_middleware';
|
|
29
|
+
this.framework = opts.framework ?? 'express';
|
|
30
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
31
|
+
this.exporter = opts.exporter ?? resolveObservationExporter();
|
|
32
|
+
this.onFlush = opts.onFlush;
|
|
33
|
+
}
|
|
34
|
+
get enabled() {
|
|
35
|
+
return configReady(this.config).ok;
|
|
36
|
+
}
|
|
37
|
+
ensureOutboundMeter() {
|
|
38
|
+
if (this.outboundInstalled || !this.enabled)
|
|
39
|
+
return;
|
|
40
|
+
this.outboundInstalled = true;
|
|
41
|
+
try {
|
|
42
|
+
const apiHost = new URL(this.config.apiUrl).hostname;
|
|
43
|
+
installOutboundInstrumentation({
|
|
44
|
+
excludedHosts: apiHost ? [apiHost] : [],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
installOutboundInstrumentation();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Prefer unwrapped fetch so telemetry uploads are never metered. */
|
|
52
|
+
uploadFetch() {
|
|
53
|
+
return getOriginalFetch() ?? this.fetchImpl;
|
|
54
|
+
}
|
|
55
|
+
start() {
|
|
56
|
+
if (this.started)
|
|
57
|
+
return;
|
|
58
|
+
this.started = true;
|
|
59
|
+
if (!this.enabled)
|
|
60
|
+
return;
|
|
61
|
+
markHooksActive({
|
|
62
|
+
processKind: this.framework,
|
|
63
|
+
exportMode: this.exporter.mode,
|
|
64
|
+
});
|
|
65
|
+
noteExportMode(this.exporter.mode);
|
|
66
|
+
this.ensureOutboundMeter();
|
|
67
|
+
this.timer = setInterval(() => {
|
|
68
|
+
void this.flush('periodic');
|
|
69
|
+
}, this.config.flushIntervalMs);
|
|
70
|
+
// Do not keep the process alive solely for the timer.
|
|
71
|
+
this.timer.unref?.();
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
if (this.timer) {
|
|
75
|
+
clearInterval(this.timer);
|
|
76
|
+
this.timer = null;
|
|
77
|
+
}
|
|
78
|
+
this.started = false;
|
|
79
|
+
}
|
|
80
|
+
recordHttp(sample) {
|
|
81
|
+
if (!this.enabled)
|
|
82
|
+
return;
|
|
83
|
+
this.ensureOutboundMeter();
|
|
84
|
+
markHooksActive({
|
|
85
|
+
processKind: this.framework,
|
|
86
|
+
exportMode: this.exporter.mode,
|
|
87
|
+
});
|
|
88
|
+
const serviceUnit = `${sample.method.toUpperCase()} ${sample.path}`;
|
|
89
|
+
this.aggregator.record({
|
|
90
|
+
serviceUnit,
|
|
91
|
+
unitType: 'http_route',
|
|
92
|
+
durationMs: sample.durationMs,
|
|
93
|
+
error: sample.statusCode >= 400,
|
|
94
|
+
tenantId: getTenantContext()?.tenantId ?? null,
|
|
95
|
+
...(sample.cpuTimeMs !== undefined ? { cpuTimeMs: sample.cpuTimeMs } : {}),
|
|
96
|
+
...(sample.peakRssKb !== undefined ? { peakRssKb: sample.peakRssKb } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Record a background job / worker task sample (BullMQ, cron, scripts). */
|
|
100
|
+
recordTask(sample) {
|
|
101
|
+
if (!this.enabled)
|
|
102
|
+
return;
|
|
103
|
+
this.ensureOutboundMeter();
|
|
104
|
+
markHooksActive({
|
|
105
|
+
processKind: this.framework,
|
|
106
|
+
exportMode: this.exporter.mode,
|
|
107
|
+
});
|
|
108
|
+
this.aggregator.record({
|
|
109
|
+
serviceUnit: sample.serviceUnit,
|
|
110
|
+
unitType: 'task',
|
|
111
|
+
durationMs: sample.durationMs,
|
|
112
|
+
error: Boolean(sample.error),
|
|
113
|
+
tenantId: sample.tenantId ?? getTenantContext()?.tenantId ?? null,
|
|
114
|
+
...(sample.cpuTimeMs !== undefined ? { cpuTimeMs: sample.cpuTimeMs } : {}),
|
|
115
|
+
...(sample.peakRssKb !== undefined ? { peakRssKb: sample.peakRssKb } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async flush(kind = 'manual') {
|
|
119
|
+
if (!this.enabled)
|
|
120
|
+
return null;
|
|
121
|
+
if (this.flushing) {
|
|
122
|
+
noteFlushSkippedInFlight();
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const externalAgg = getExternalAggregator();
|
|
126
|
+
const hasRuntime = this.aggregator.hasData();
|
|
127
|
+
const hasExternal = externalAgg.hasData();
|
|
128
|
+
if (!hasRuntime && !hasExternal)
|
|
129
|
+
return null;
|
|
130
|
+
this.flushing = true;
|
|
131
|
+
noteExportMode(this.exporter.mode);
|
|
132
|
+
const snapshot = hasRuntime ? this.aggregator.takeSnapshot() : null;
|
|
133
|
+
const externalSnapshot = hasExternal ? externalAgg.takeSnapshot() : null;
|
|
134
|
+
try {
|
|
135
|
+
const windowStartedMs = snapshot
|
|
136
|
+
? snapshot.windowStartedMs()
|
|
137
|
+
: Date.now() - 1000;
|
|
138
|
+
const windowSeconds = Math.max(0.001, (Date.now() - windowStartedMs) / 1000);
|
|
139
|
+
// Crude trend energy: TDP * utilization proxy from request busy time.
|
|
140
|
+
const busySeconds = snapshot
|
|
141
|
+
? snapshot.hasMeasuredCpu()
|
|
142
|
+
? snapshot.totalMeasuredCpuMs() / 1000
|
|
143
|
+
: snapshot.totalDurationMs() / 1000
|
|
144
|
+
: 0;
|
|
145
|
+
const util = Math.min(1, busySeconds / Math.max(windowSeconds, 0.001));
|
|
146
|
+
const windowEnergyKwh = (this.config.estimateTdpWatts * util * windowSeconds) / 1000 / 3600;
|
|
147
|
+
const runtimeV0 = snapshot
|
|
148
|
+
? snapshot.buildV0({
|
|
149
|
+
collector: this.collector,
|
|
150
|
+
framework: this.framework,
|
|
151
|
+
windowEnergyKwh,
|
|
152
|
+
windowSeconds,
|
|
153
|
+
})
|
|
154
|
+
: null;
|
|
155
|
+
if (snapshot && !runtimeV0) {
|
|
156
|
+
this.aggregator.mergeSnapshot(snapshot);
|
|
157
|
+
if (externalSnapshot)
|
|
158
|
+
externalAgg.mergeSnapshot(externalSnapshot);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const externalV0 = externalSnapshot ? externalSnapshot.buildV0() : null;
|
|
162
|
+
const end = new Date();
|
|
163
|
+
const start = new Date(end.getTime() - windowSeconds * 1000);
|
|
164
|
+
const windowId = newRuntimeWindowId();
|
|
165
|
+
// Prefer effective RuntimeConfig over ambient process.env.
|
|
166
|
+
const tenantConfig = tenantConfigFromRuntime(this.config);
|
|
167
|
+
const runContext = {
|
|
168
|
+
client: 'netgreener_node_runtime',
|
|
169
|
+
runtime_flush_kind: kind,
|
|
170
|
+
measured_scope: 'process',
|
|
171
|
+
resource_scope: 'process',
|
|
172
|
+
...(this.config.deployEnvironment
|
|
173
|
+
? { deploy_environment: this.config.deployEnvironment }
|
|
174
|
+
: {}),
|
|
175
|
+
...(this.config.releaseTag ? { release_tag: this.config.releaseTag } : {}),
|
|
176
|
+
};
|
|
177
|
+
attachWindowTenantToRunContext(runContext, {
|
|
178
|
+
runtimeV0: runtimeV0 ?? undefined,
|
|
179
|
+
externalV0: externalV0 ?? undefined,
|
|
180
|
+
tenantConfig,
|
|
181
|
+
activeContext: getTenantContext(),
|
|
182
|
+
});
|
|
183
|
+
const measuredCpu = snapshot?.hasMeasuredCpu() ?? false;
|
|
184
|
+
const measuredRss = snapshot?.hasMeasuredRss() ?? false;
|
|
185
|
+
const session_metadata = {
|
|
186
|
+
run_context: runContext,
|
|
187
|
+
...(runtimeV0 ? { service_runtime_v0: runtimeV0 } : {}),
|
|
188
|
+
...(externalV0 ? { external_api_v0: externalV0 } : {}),
|
|
189
|
+
measurement_provenance: {
|
|
190
|
+
cpu_source: runtimeV0
|
|
191
|
+
? measuredCpu
|
|
192
|
+
? CPU_SOURCE_PROCESS
|
|
193
|
+
: CPU_SOURCE_DURATION_PROXY
|
|
194
|
+
: 'unavailable',
|
|
195
|
+
memory_source: measuredRss ? MEMORY_SOURCE_PROCESS : 'unavailable',
|
|
196
|
+
energy_model: runtimeV0 ? 'tdp_utilization_estimate' : 'unavailable',
|
|
197
|
+
gpu_scope: 'none',
|
|
198
|
+
gpu_source: 'unavailable',
|
|
199
|
+
carbon_factor: {
|
|
200
|
+
value: 0.4,
|
|
201
|
+
unit: 'kg_co2e_per_kwh',
|
|
202
|
+
source: 'default_grid_factor',
|
|
203
|
+
},
|
|
204
|
+
evidence_grades: {
|
|
205
|
+
schema_version: 1,
|
|
206
|
+
policy_version: 'r9_v0',
|
|
207
|
+
metrics: {
|
|
208
|
+
local_energy: {
|
|
209
|
+
metric: 'local_energy',
|
|
210
|
+
grade: runtimeV0 ? 'G1' : 'G0',
|
|
211
|
+
reasons: runtimeV0 ? ['estimated_power_tdp'] : ['no_runtime_units'],
|
|
212
|
+
},
|
|
213
|
+
local_carbon: {
|
|
214
|
+
metric: 'local_carbon',
|
|
215
|
+
grade: runtimeV0 ? 'G1' : 'G0',
|
|
216
|
+
reasons: runtimeV0
|
|
217
|
+
? ['estimated_power_tdp', 'default_grid_factor']
|
|
218
|
+
: ['no_runtime_units'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
combined_floor_grade: runtimeV0 ? 'G1' : 'G0',
|
|
222
|
+
notes: ['vendor_carbon_separate_ledger'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
mergeHealthIntoMetadata(session_metadata, buildRuntimeHealthV0());
|
|
227
|
+
const payload = {
|
|
228
|
+
project_id: this.config.projectId,
|
|
229
|
+
start_time: start.toISOString(),
|
|
230
|
+
end_time: end.toISOString(),
|
|
231
|
+
server_name: hostname() || null,
|
|
232
|
+
energy_kwh: windowEnergyKwh,
|
|
233
|
+
runtime_window_id: windowId,
|
|
234
|
+
session_metadata,
|
|
235
|
+
};
|
|
236
|
+
noteExportAttempt(this.exporter.mode);
|
|
237
|
+
const result = await this.exporter.exportRunSessionWindow(this.config, payload, this.uploadFetch());
|
|
238
|
+
noteExportResult(result.ok, result.ok ? undefined : 'error' in result ? result.error : 'export_failed');
|
|
239
|
+
mergeHealthIntoMetadata(session_metadata, buildRuntimeHealthV0());
|
|
240
|
+
if (!result.ok) {
|
|
241
|
+
if (snapshot)
|
|
242
|
+
this.aggregator.mergeSnapshot(snapshot);
|
|
243
|
+
if (externalSnapshot)
|
|
244
|
+
externalAgg.mergeSnapshot(externalSnapshot);
|
|
245
|
+
}
|
|
246
|
+
this.onFlush?.(result, payload);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
noteExportResult(false, err instanceof Error ? err.message : String(err));
|
|
251
|
+
if (snapshot)
|
|
252
|
+
this.aggregator.mergeSnapshot(snapshot);
|
|
253
|
+
if (externalSnapshot)
|
|
254
|
+
externalAgg.mergeSnapshot(externalSnapshot);
|
|
255
|
+
const result = {
|
|
256
|
+
ok: false,
|
|
257
|
+
error: err instanceof Error ? err.message : String(err),
|
|
258
|
+
};
|
|
259
|
+
this.onFlush?.(result, {
|
|
260
|
+
project_id: this.config.projectId,
|
|
261
|
+
start_time: new Date().toISOString(),
|
|
262
|
+
end_time: new Date().toISOString(),
|
|
263
|
+
runtime_window_id: newRuntimeWindowId(),
|
|
264
|
+
session_metadata: {},
|
|
265
|
+
});
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.flushing = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async shutdown() {
|
|
273
|
+
this.stop();
|
|
274
|
+
return this.flush('shutdown');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let globalRuntime = null;
|
|
278
|
+
export function getRuntime(opts) {
|
|
279
|
+
if (!globalRuntime) {
|
|
280
|
+
globalRuntime = new NetGreenerRuntime(opts);
|
|
281
|
+
}
|
|
282
|
+
return globalRuntime;
|
|
283
|
+
}
|
|
284
|
+
/** Test helper — reset singleton. */
|
|
285
|
+
export function _resetRuntimeForTests() {
|
|
286
|
+
globalRuntime?.stop();
|
|
287
|
+
globalRuntime = null;
|
|
288
|
+
_resetRuntimeHealthForTests();
|
|
289
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime / export health (N6 / CAP-R5 honesty).
|
|
3
|
+
*
|
|
4
|
+
* Additive ``runtime_health_v0`` twin of Python ``netgreener.runtime_health``.
|
|
5
|
+
* Does not change upload defaults or invent collector spool metrics.
|
|
6
|
+
*/
|
|
7
|
+
export type RuntimeHealthStatus = 'ok' | 'degraded' | 'conflict' | 'unknown';
|
|
8
|
+
export type RuntimeHealthV0 = {
|
|
9
|
+
schema_version: 1;
|
|
10
|
+
status: RuntimeHealthStatus;
|
|
11
|
+
codes: string[];
|
|
12
|
+
detail: string;
|
|
13
|
+
remediation: string[];
|
|
14
|
+
bootstrap: string;
|
|
15
|
+
hooks_active: boolean;
|
|
16
|
+
process_kind: string;
|
|
17
|
+
collector_mode: string | null;
|
|
18
|
+
export_mode: string;
|
|
19
|
+
export_attempts: number;
|
|
20
|
+
export_ok: number;
|
|
21
|
+
export_fail: number;
|
|
22
|
+
/** Flushes skipped because another flush was in flight (best-effort drop). */
|
|
23
|
+
export_skipped_in_flight: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function _resetRuntimeHealthForTests(): void;
|
|
26
|
+
export declare function markHooksActive(opts?: {
|
|
27
|
+
processKind?: string;
|
|
28
|
+
exportMode?: string;
|
|
29
|
+
}): void;
|
|
30
|
+
export declare function noteExportMode(mode: string): void;
|
|
31
|
+
export declare function noteFlushSkippedInFlight(): void;
|
|
32
|
+
export declare function noteExportAttempt(mode: string): void;
|
|
33
|
+
export declare function noteExportResult(ok: boolean, error?: string): void;
|
|
34
|
+
export declare function buildRuntimeHealthV0(): RuntimeHealthV0;
|
|
35
|
+
export declare function mergeHealthIntoMetadata(meta: Record<string, unknown>, health?: RuntimeHealthV0): void;
|