nucleus-core-ts 0.9.946 → 0.9.948
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/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.948
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ and limitations under the License.
|
|
|
71
71
|
</div>
|
|
72
72
|
</body>
|
|
73
73
|
</html>
|
|
74
|
-
`}getActiveAlerts(){return Array.from(this.state.activeAlerts.values())}acknowledgeAlert(alertId){for(let[_type,alert]of this.state.activeAlerts)if(alert.id===alertId)return alert.acknowledged=!0,!0;return!1}clearAlert(type){this.state.activeAlerts.delete(type)}}var init_AlertService=()=>{};class ApplicationCollector{config;requestCount=0;responseTimes=[];errorCount=0;rateLimitBlocks=0;byEndpoint={};byMethod={};byStatus={};byErrorType={};lastCollectTime=Date.now();constructor(config){this.config=config}recordRequest(params){if(!this.config?.enabled)return;
|
|
74
|
+
`}getActiveAlerts(){return Array.from(this.state.activeAlerts.values())}acknowledgeAlert(alertId){for(let[_type,alert]of this.state.activeAlerts)if(alert.id===alertId)return alert.acknowledged=!0,!0;return!1}clearAlert(type){this.state.activeAlerts.delete(type)}}var init_AlertService=()=>{};class ApplicationCollector{config;requestCount=0;responseTimes=[];errorCount=0;rateLimitBlocks=0;byEndpoint={};byMethod={};byStatus={};byErrorType={};lastCollectTime=Date.now();buckets=[];constructor(config){this.config=config}bucketFor(second){let index=second%60,bucket=this.buckets[index];if(!bucket)return bucket={second,requests:0,errors:0,blocks:0},this.buckets[index]=bucket,bucket;if(bucket.second!==second)bucket.second=second,bucket.requests=0,bucket.errors=0,bucket.blocks=0;return bucket}windowed(pick){let oldest=Math.floor(Date.now()/1000)-60,total=0;for(let bucket of this.buckets)if(bucket&&bucket.second>oldest)total+=pick(bucket);return total}recordRequest(params){if(!this.config?.enabled)return;this.requestCount++;let bucket=this.bucketFor(Math.floor(Date.now()/1000));if(bucket.requests+=1,params.isError)bucket.errors+=1;if(this.config.metrics?.responseTime!==!1){if(this.responseTimes.push({at:Date.now(),ms:params.responseTimeMs}),this.responseTimes.length>50000)this.pruneResponseTimes()}if(this.config.metrics?.requests!==!1)this.byEndpoint[params.endpoint]=(this.byEndpoint[params.endpoint]||0)+1,this.byMethod[params.method]=(this.byMethod[params.method]||0)+1,this.byStatus[String(params.status)]=(this.byStatus[String(params.status)]||0)+1;if(this.config.metrics?.errors!==!1&¶ms.isError){if(this.errorCount++,params.errorType)this.byErrorType[params.errorType]=(this.byErrorType[params.errorType]||0)+1}}recordRateLimitBlock(){if(!this.config?.enabled||this.config.metrics?.rateLimits===!1)return;this.rateLimitBlocks++,this.bucketFor(Math.floor(Date.now()/1000)).blocks+=1}pruneResponseTimes(){let oldest=Date.now()-60000,kept=this.responseTimes.filter((sample)=>sample.at>oldest);if(kept.length>50000)kept=kept.slice(-50000);this.responseTimes=kept}collect(){if(!this.config?.enabled)return null;let perMinute=this.windowed((b)=>b.requests),blockedPerMinute=this.windowed((b)=>b.blocks);this.pruneResponseTimes();let sortedTimes=this.responseTimes.map((s)=>s.ms).sort((a,b)=>a-b),len=sortedTimes.length;return{requests:{total:this.requestCount,perMinute,byEndpoint:{...this.byEndpoint},byMethod:{...this.byMethod},byStatus:{...this.byStatus}},responseTime:{avg:len>0?Math.round(sortedTimes.reduce((a,b)=>a+b,0)/len*100)/100:0,min:len>0?sortedTimes[0]??0:0,max:len>0?sortedTimes[len-1]??0:0,p50:len>0?sortedTimes[Math.floor(len*0.5)]??0:0,p95:len>0?sortedTimes[Math.floor(len*0.95)]??0:0,p99:len>0?sortedTimes[Math.floor(len*0.99)]??0:0},errors:{total:this.errorCount,rate:(()=>{let requests=perMinute;if(requests===0)return 0;let errors=this.windowed((b)=>b.errors);return Math.round(errors/requests*100*100)/100})(),byType:{...this.byErrorType}},rateLimits:{blocked:this.rateLimitBlocks,blockedPerMinute}}}reset(){this.requestCount=0,this.responseTimes=[],this.errorCount=0,this.rateLimitBlocks=0,this.byEndpoint={},this.byMethod={},this.byStatus={},this.byErrorType={},this.buckets=[],this.lastCollectTime=Date.now()}getRequestsPerMinute(){return this.windowed((b)=>b.requests)}getErrorRate(){let requests=this.windowed((b)=>b.requests);return requests>0?this.windowed((b)=>b.errors)/requests*100:0}getRateLimitBlocksPerMinute(){return this.windowed((b)=>b.blocks)}getAvgResponseTime(){if(this.pruneResponseTimes(),this.responseTimes.length===0)return 0;return this.responseTimes.reduce((total,s)=>total+s.ms,0)/this.responseTimes.length}}class DatabaseCollector{config;execute;lastTotals=null;activeTimeSupported=!0;constructor(config,execute){this.config=config,this.execute=execute}async collect(){if(!this.config.enabled)return null;let metrics={connections:{active:0,idle:0,total:0},queries:{total:0,avgTime:0,slowQueries:0}};if(this.config.metrics?.connections!==!1)metrics.connections=await this.collectConnections();if(this.config.metrics?.queryTime!==!1)metrics.queries=await this.collectQueries();return metrics}toNumber(value){let num=Number.parseFloat(String(value??0));return Number.isNaN(num)?0:num}async collectConnections(){try{let row=(await this.execute(`SELECT
|
|
75
75
|
count(*) FILTER (WHERE state = 'active') AS active,
|
|
76
76
|
count(*) FILTER (WHERE state = 'idle') AS idle,
|
|
77
77
|
count(*) AS total
|
|
@@ -2,6 +2,16 @@ import type { ApplicationMetrics, MonitoringConfig } from '../types';
|
|
|
2
2
|
export declare class ApplicationCollector {
|
|
3
3
|
private config;
|
|
4
4
|
private requestCount;
|
|
5
|
+
/**
|
|
6
|
+
* Response times with the moment they happened, so percentiles describe NOW.
|
|
7
|
+
*
|
|
8
|
+
* They used to be a plain list of the last 5–10.000 numbers. On a quiet
|
|
9
|
+
* install that is not a window, it is a memory: run a load test at lunchtime
|
|
10
|
+
* and the p95 on the screen still describes the load test at six in the
|
|
11
|
+
* evening, because nothing has come along to push those samples out. The
|
|
12
|
+
* peak recorder then writes that stale p95 into a peak's context as "what
|
|
13
|
+
* the response time was at the time", which it was not.
|
|
14
|
+
*/
|
|
5
15
|
private responseTimes;
|
|
6
16
|
private errorCount;
|
|
7
17
|
private rateLimitBlocks;
|
|
@@ -10,7 +20,34 @@ export declare class ApplicationCollector {
|
|
|
10
20
|
private byStatus;
|
|
11
21
|
private byErrorType;
|
|
12
22
|
private lastCollectTime;
|
|
23
|
+
/**
|
|
24
|
+
* The last sixty seconds, one bucket each.
|
|
25
|
+
*
|
|
26
|
+
* ## What this replaces, and why it is not a detail
|
|
27
|
+
*
|
|
28
|
+
* `perMinute` used to be `requestCount / minutesSinceBoot` — the lifetime
|
|
29
|
+
* AVERAGE, presented as the current rate, on a chart labelled "requests per
|
|
30
|
+
* minute" and in the alert thresholds. `reset()` exists to make that
|
|
31
|
+
* arithmetic correct and is called from nowhere, so the divisor kept growing
|
|
32
|
+
* for as long as the pod lived.
|
|
33
|
+
*
|
|
34
|
+
* What that does to a real number: measured on 6 August 2026, a load run
|
|
35
|
+
* driving roughly fifty API requests a SECOND was recorded as a peak of 273
|
|
36
|
+
* per MINUTE, in the same snapshot that reported 370 cumulative hits on the
|
|
37
|
+
* busiest endpoint alone — two figures that cannot both be true. A pod up for
|
|
38
|
+
* a week would have flattened a genuine incident into nothing at all, which
|
|
39
|
+
* is the same failure in the direction that matters: the alert that never
|
|
40
|
+
* fires.
|
|
41
|
+
*
|
|
42
|
+
* A rolling window costs sixty small objects and answers the question the
|
|
43
|
+
* metric's name promises.
|
|
44
|
+
*/
|
|
45
|
+
private buckets;
|
|
13
46
|
constructor(config: MonitoringConfig['application']);
|
|
47
|
+
/** The bucket for `now`, recycled if this slot belongs to an expired second. */
|
|
48
|
+
private bucketFor;
|
|
49
|
+
/** Sums the buckets still inside the window. */
|
|
50
|
+
private windowed;
|
|
14
51
|
recordRequest(params: {
|
|
15
52
|
endpoint: string;
|
|
16
53
|
method: string;
|
|
@@ -20,10 +57,26 @@ export declare class ApplicationCollector {
|
|
|
20
57
|
errorType?: string;
|
|
21
58
|
}): void;
|
|
22
59
|
recordRateLimitBlock(): void;
|
|
60
|
+
/** Drops samples that have fallen out of the window, then out of the cap. */
|
|
61
|
+
private pruneResponseTimes;
|
|
23
62
|
collect(): ApplicationMetrics | null;
|
|
63
|
+
/**
|
|
64
|
+
* Clears the cumulative counters.
|
|
65
|
+
*
|
|
66
|
+
* Kept for callers that want a fresh baseline, but nothing on the boot path
|
|
67
|
+
* calls it — and that used to be a silent bug rather than a choice, because
|
|
68
|
+
* the rate arithmetic depended on it. The rates no longer do: they read the
|
|
69
|
+
* rolling window, which expires on its own.
|
|
70
|
+
*/
|
|
24
71
|
reset(): void;
|
|
72
|
+
/** Requests in the last sixty seconds. */
|
|
25
73
|
getRequestsPerMinute(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Error rate over the last minute — the same window the alert thresholds
|
|
76
|
+
* should be judging, and the same one `collect()` reports.
|
|
77
|
+
*/
|
|
26
78
|
getErrorRate(): number;
|
|
27
79
|
getRateLimitBlocksPerMinute(): number;
|
|
80
|
+
/** Mean response time over the same minute the other rates describe. */
|
|
28
81
|
getAvgResponseTime(): number;
|
|
29
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.948",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|