@contractspec/lib.cost-tracking 1.57.0 → 1.59.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/dist/browser/index.js +163 -0
- package/dist/budget-alert-manager.d.ts +16 -20
- package/dist/budget-alert-manager.d.ts.map +1 -1
- package/dist/cost-model.d.ts +9 -13
- package/dist/cost-model.d.ts.map +1 -1
- package/dist/cost-tracker.d.ts +15 -19
- package/dist/cost-tracker.d.ts.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +164 -6
- package/dist/node/index.js +163 -0
- package/dist/optimization-recommender.d.ts +3 -7
- package/dist/optimization-recommender.d.ts.map +1 -1
- package/dist/types.d.ts +52 -55
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -14
- package/dist/budget-alert-manager.js +0 -33
- package/dist/budget-alert-manager.js.map +0 -1
- package/dist/cost-model.js +0 -22
- package/dist/cost-model.js.map +0 -1
- package/dist/cost-tracker.js +0 -61
- package/dist/cost-tracker.js.map +0 -1
- package/dist/optimization-recommender.js +0 -40
- package/dist/optimization-recommender.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/cost-model.ts
|
|
2
|
+
var defaultCostModel = {
|
|
3
|
+
dbReadCost: 0.000002,
|
|
4
|
+
dbWriteCost: 0.00001,
|
|
5
|
+
computeMsCost: 0.00000015,
|
|
6
|
+
memoryMbMsCost: 0.00000002
|
|
7
|
+
};
|
|
8
|
+
function calculateSampleCost(sample, model) {
|
|
9
|
+
const external = (sample.externalCalls ?? []).reduce((sum, call) => sum + (call.cost ?? 0), 0);
|
|
10
|
+
return {
|
|
11
|
+
dbReads: (sample.dbReads ?? 0) * model.dbReadCost,
|
|
12
|
+
dbWrites: (sample.dbWrites ?? 0) * model.dbWriteCost,
|
|
13
|
+
compute: (sample.computeMs ?? 0) * model.computeMsCost,
|
|
14
|
+
memory: (sample.memoryMbMs ?? 0) * model.memoryMbMsCost,
|
|
15
|
+
external,
|
|
16
|
+
custom: sample.customCost ?? 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// src/cost-tracker.ts
|
|
20
|
+
class CostTracker {
|
|
21
|
+
options;
|
|
22
|
+
totals = new Map;
|
|
23
|
+
costModel;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.costModel = options.costModel ?? defaultCostModel;
|
|
27
|
+
}
|
|
28
|
+
recordSample(sample) {
|
|
29
|
+
const breakdown = calculateSampleCost(sample, this.costModel);
|
|
30
|
+
const total = breakdown.dbReads + breakdown.dbWrites + breakdown.compute + breakdown.memory + breakdown.external + breakdown.custom;
|
|
31
|
+
const key = this.buildKey(sample.operation, sample.tenantId);
|
|
32
|
+
const existing = this.totals.get(key);
|
|
33
|
+
const summary = existing ? {
|
|
34
|
+
...existing,
|
|
35
|
+
total: existing.total + total,
|
|
36
|
+
breakdown: {
|
|
37
|
+
dbReads: existing.breakdown.dbReads + breakdown.dbReads,
|
|
38
|
+
dbWrites: existing.breakdown.dbWrites + breakdown.dbWrites,
|
|
39
|
+
compute: existing.breakdown.compute + breakdown.compute,
|
|
40
|
+
memory: existing.breakdown.memory + breakdown.memory,
|
|
41
|
+
external: existing.breakdown.external + breakdown.external,
|
|
42
|
+
custom: existing.breakdown.custom + breakdown.custom
|
|
43
|
+
},
|
|
44
|
+
samples: existing.samples + 1
|
|
45
|
+
} : {
|
|
46
|
+
operation: sample.operation,
|
|
47
|
+
tenantId: sample.tenantId,
|
|
48
|
+
total,
|
|
49
|
+
breakdown: {
|
|
50
|
+
dbReads: breakdown.dbReads,
|
|
51
|
+
dbWrites: breakdown.dbWrites,
|
|
52
|
+
compute: breakdown.compute,
|
|
53
|
+
memory: breakdown.memory,
|
|
54
|
+
external: breakdown.external,
|
|
55
|
+
custom: breakdown.custom
|
|
56
|
+
},
|
|
57
|
+
samples: 1
|
|
58
|
+
};
|
|
59
|
+
this.totals.set(key, summary);
|
|
60
|
+
this.options.onSampleRecorded?.(sample, total);
|
|
61
|
+
return summary;
|
|
62
|
+
}
|
|
63
|
+
getTotals(filter) {
|
|
64
|
+
const items = Array.from(this.totals.values());
|
|
65
|
+
if (!filter?.tenantId) {
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
return items.filter((item) => item.tenantId === filter.tenantId);
|
|
69
|
+
}
|
|
70
|
+
reset() {
|
|
71
|
+
this.totals.clear();
|
|
72
|
+
}
|
|
73
|
+
buildKey(operation, tenantId) {
|
|
74
|
+
return tenantId ? `${tenantId}:${operation}` : operation;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// src/budget-alert-manager.ts
|
|
78
|
+
class BudgetAlertManager {
|
|
79
|
+
options;
|
|
80
|
+
limits = new Map;
|
|
81
|
+
spend = new Map;
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.options = options;
|
|
84
|
+
for (const budget of options.budgets) {
|
|
85
|
+
this.limits.set(budget.tenantId, budget);
|
|
86
|
+
this.spend.set(budget.tenantId, 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
track(summary) {
|
|
90
|
+
if (!summary.tenantId) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const current = (this.spend.get(summary.tenantId) ?? 0) + summary.total;
|
|
94
|
+
this.spend.set(summary.tenantId, current);
|
|
95
|
+
const budget = this.limits.get(summary.tenantId);
|
|
96
|
+
if (!budget) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const threshold = budget.alertThreshold ?? 0.8;
|
|
100
|
+
if (current >= budget.monthlyLimit * threshold) {
|
|
101
|
+
this.options.onAlert?.({
|
|
102
|
+
tenantId: summary.tenantId,
|
|
103
|
+
limit: budget.monthlyLimit,
|
|
104
|
+
total: current,
|
|
105
|
+
summary
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
getSpend(tenantId) {
|
|
110
|
+
return this.spend.get(tenantId) ?? 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// src/optimization-recommender.ts
|
|
114
|
+
class OptimizationRecommender {
|
|
115
|
+
generate(summary) {
|
|
116
|
+
const suggestions = [];
|
|
117
|
+
const avgCost = summary.total / summary.samples;
|
|
118
|
+
if (summary.breakdown.dbReads / summary.samples > 1000) {
|
|
119
|
+
suggestions.push({
|
|
120
|
+
operation: summary.operation,
|
|
121
|
+
tenantId: summary.tenantId,
|
|
122
|
+
category: "n_plus_one",
|
|
123
|
+
message: "High average DB read count detected. Consider batching queries or adding pagination.",
|
|
124
|
+
evidence: { avgReads: summary.breakdown.dbReads / summary.samples }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (summary.breakdown.compute / summary.total > 0.6) {
|
|
128
|
+
suggestions.push({
|
|
129
|
+
operation: summary.operation,
|
|
130
|
+
tenantId: summary.tenantId,
|
|
131
|
+
category: "batching",
|
|
132
|
+
message: "Compute dominates cost. Investigate hot loops or move heavy logic to background jobs.",
|
|
133
|
+
evidence: { computeShare: summary.breakdown.compute / summary.total }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (summary.breakdown.external > avgCost * 0.5) {
|
|
137
|
+
suggestions.push({
|
|
138
|
+
operation: summary.operation,
|
|
139
|
+
tenantId: summary.tenantId,
|
|
140
|
+
category: "external",
|
|
141
|
+
message: "External provider spend is high. Reuse results or enable caching.",
|
|
142
|
+
evidence: { externalCost: summary.breakdown.external }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (summary.breakdown.memory > summary.breakdown.compute * 1.2) {
|
|
146
|
+
suggestions.push({
|
|
147
|
+
operation: summary.operation,
|
|
148
|
+
tenantId: summary.tenantId,
|
|
149
|
+
category: "caching",
|
|
150
|
+
message: "Memory utilization suggests cached payloads linger. Tune TTL or stream responses.",
|
|
151
|
+
evidence: { memoryCost: summary.breakdown.memory }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return suggestions;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
defaultCostModel,
|
|
159
|
+
calculateSampleCost,
|
|
160
|
+
OptimizationRecommender,
|
|
161
|
+
CostTracker,
|
|
162
|
+
BudgetAlertManager
|
|
163
|
+
};
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import { OperationCostSummary, TenantBudget } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
summary: OperationCostSummary;
|
|
11
|
-
}) => void;
|
|
1
|
+
import type { OperationCostSummary, TenantBudget } from './types';
|
|
2
|
+
export interface BudgetAlertManagerOptions {
|
|
3
|
+
budgets: TenantBudget[];
|
|
4
|
+
onAlert?: (payload: {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
limit: number;
|
|
7
|
+
total: number;
|
|
8
|
+
summary: OperationCostSummary;
|
|
9
|
+
}) => void;
|
|
12
10
|
}
|
|
13
|
-
declare class BudgetAlertManager {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
export declare class BudgetAlertManager {
|
|
12
|
+
private readonly options;
|
|
13
|
+
private readonly limits;
|
|
14
|
+
private readonly spend;
|
|
15
|
+
constructor(options: BudgetAlertManagerOptions);
|
|
16
|
+
track(summary: OperationCostSummary): void;
|
|
17
|
+
getSpend(tenantId: string): number;
|
|
20
18
|
}
|
|
21
|
-
//#endregion
|
|
22
|
-
export { BudgetAlertManager, BudgetAlertManagerOptions };
|
|
23
19
|
//# sourceMappingURL=budget-alert-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budget-alert-manager.d.ts","
|
|
1
|
+
{"version":3,"file":"budget-alert-manager.d.ts","sourceRoot":"","sources":["../src/budget-alert-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAElE,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,oBAAoB,CAAC;KAC/B,KAAK,IAAI,CAAC;CACZ;AAED,qBAAa,kBAAkB;IAIjB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;gBAEtB,OAAO,EAAE,yBAAyB;IAO/D,KAAK,CAAC,OAAO,EAAE,oBAAoB;IAwBnC,QAAQ,CAAC,QAAQ,EAAE,MAAM;CAG1B"}
|
package/dist/cost-model.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { CostModel, CostSample } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
external: number;
|
|
11
|
-
custom: number;
|
|
1
|
+
import type { CostModel, CostSample } from './types';
|
|
2
|
+
export declare const defaultCostModel: CostModel;
|
|
3
|
+
export declare function calculateSampleCost(sample: CostSample, model: CostModel): {
|
|
4
|
+
dbReads: number;
|
|
5
|
+
dbWrites: number;
|
|
6
|
+
compute: number;
|
|
7
|
+
memory: number;
|
|
8
|
+
external: number;
|
|
9
|
+
custom: number;
|
|
12
10
|
};
|
|
13
|
-
//#endregion
|
|
14
|
-
export { calculateSampleCost, defaultCostModel };
|
|
15
11
|
//# sourceMappingURL=cost-model.d.ts.map
|
package/dist/cost-model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost-model.d.ts","
|
|
1
|
+
{"version":3,"file":"cost-model.d.ts","sourceRoot":"","sources":["../src/cost-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErD,eAAO,MAAM,gBAAgB,EAAE,SAK9B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS;;;;;;;EAcvE"}
|
package/dist/cost-tracker.d.ts
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import { CostModel, CostSample, OperationCostSummary } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
costModel?: CostModel;
|
|
6
|
-
onSampleRecorded?: (sample: CostSample, total: number) => void;
|
|
1
|
+
import type { CostModel, CostSample, OperationCostSummary } from './types';
|
|
2
|
+
export interface CostTrackerOptions {
|
|
3
|
+
costModel?: CostModel;
|
|
4
|
+
onSampleRecorded?: (sample: CostSample, total: number) => void;
|
|
7
5
|
}
|
|
8
|
-
declare class CostTracker {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
export declare class CostTracker {
|
|
7
|
+
private readonly options;
|
|
8
|
+
private readonly totals;
|
|
9
|
+
private readonly costModel;
|
|
10
|
+
constructor(options?: CostTrackerOptions);
|
|
11
|
+
recordSample(sample: CostSample): OperationCostSummary;
|
|
12
|
+
getTotals(filter?: {
|
|
13
|
+
tenantId?: string;
|
|
14
|
+
}): OperationCostSummary[];
|
|
15
|
+
reset(): void;
|
|
16
|
+
private buildKey;
|
|
19
17
|
}
|
|
20
|
-
//#endregion
|
|
21
|
-
export { CostTracker, CostTrackerOptions };
|
|
22
18
|
//# sourceMappingURL=cost-tracker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost-tracker.d.ts","
|
|
1
|
+
{"version":3,"file":"cost-tracker.d.ts","sourceRoot":"","sources":["../src/cost-tracker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE3E,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAChE;AAED,qBAAa,WAAW;IAIV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2C;IAClE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAET,OAAO,GAAE,kBAAuB;IAI7D,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,oBAAoB;IA+CtD,SAAS,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAQxC,KAAK;IAIL,OAAO,CAAC,QAAQ;CAGjB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './cost-model';
|
|
3
|
+
export * from './cost-tracker';
|
|
4
|
+
export * from './budget-alert-manager';
|
|
5
|
+
export * from './optimization-recommender';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/cost-model.ts
|
|
3
|
+
var defaultCostModel = {
|
|
4
|
+
dbReadCost: 0.000002,
|
|
5
|
+
dbWriteCost: 0.00001,
|
|
6
|
+
computeMsCost: 0.00000015,
|
|
7
|
+
memoryMbMsCost: 0.00000002
|
|
8
|
+
};
|
|
9
|
+
function calculateSampleCost(sample, model) {
|
|
10
|
+
const external = (sample.externalCalls ?? []).reduce((sum, call) => sum + (call.cost ?? 0), 0);
|
|
11
|
+
return {
|
|
12
|
+
dbReads: (sample.dbReads ?? 0) * model.dbReadCost,
|
|
13
|
+
dbWrites: (sample.dbWrites ?? 0) * model.dbWriteCost,
|
|
14
|
+
compute: (sample.computeMs ?? 0) * model.computeMsCost,
|
|
15
|
+
memory: (sample.memoryMbMs ?? 0) * model.memoryMbMsCost,
|
|
16
|
+
external,
|
|
17
|
+
custom: sample.customCost ?? 0
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// src/cost-tracker.ts
|
|
21
|
+
class CostTracker {
|
|
22
|
+
options;
|
|
23
|
+
totals = new Map;
|
|
24
|
+
costModel;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.costModel = options.costModel ?? defaultCostModel;
|
|
28
|
+
}
|
|
29
|
+
recordSample(sample) {
|
|
30
|
+
const breakdown = calculateSampleCost(sample, this.costModel);
|
|
31
|
+
const total = breakdown.dbReads + breakdown.dbWrites + breakdown.compute + breakdown.memory + breakdown.external + breakdown.custom;
|
|
32
|
+
const key = this.buildKey(sample.operation, sample.tenantId);
|
|
33
|
+
const existing = this.totals.get(key);
|
|
34
|
+
const summary = existing ? {
|
|
35
|
+
...existing,
|
|
36
|
+
total: existing.total + total,
|
|
37
|
+
breakdown: {
|
|
38
|
+
dbReads: existing.breakdown.dbReads + breakdown.dbReads,
|
|
39
|
+
dbWrites: existing.breakdown.dbWrites + breakdown.dbWrites,
|
|
40
|
+
compute: existing.breakdown.compute + breakdown.compute,
|
|
41
|
+
memory: existing.breakdown.memory + breakdown.memory,
|
|
42
|
+
external: existing.breakdown.external + breakdown.external,
|
|
43
|
+
custom: existing.breakdown.custom + breakdown.custom
|
|
44
|
+
},
|
|
45
|
+
samples: existing.samples + 1
|
|
46
|
+
} : {
|
|
47
|
+
operation: sample.operation,
|
|
48
|
+
tenantId: sample.tenantId,
|
|
49
|
+
total,
|
|
50
|
+
breakdown: {
|
|
51
|
+
dbReads: breakdown.dbReads,
|
|
52
|
+
dbWrites: breakdown.dbWrites,
|
|
53
|
+
compute: breakdown.compute,
|
|
54
|
+
memory: breakdown.memory,
|
|
55
|
+
external: breakdown.external,
|
|
56
|
+
custom: breakdown.custom
|
|
57
|
+
},
|
|
58
|
+
samples: 1
|
|
59
|
+
};
|
|
60
|
+
this.totals.set(key, summary);
|
|
61
|
+
this.options.onSampleRecorded?.(sample, total);
|
|
62
|
+
return summary;
|
|
63
|
+
}
|
|
64
|
+
getTotals(filter) {
|
|
65
|
+
const items = Array.from(this.totals.values());
|
|
66
|
+
if (!filter?.tenantId) {
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
return items.filter((item) => item.tenantId === filter.tenantId);
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.totals.clear();
|
|
73
|
+
}
|
|
74
|
+
buildKey(operation, tenantId) {
|
|
75
|
+
return tenantId ? `${tenantId}:${operation}` : operation;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// src/budget-alert-manager.ts
|
|
79
|
+
class BudgetAlertManager {
|
|
80
|
+
options;
|
|
81
|
+
limits = new Map;
|
|
82
|
+
spend = new Map;
|
|
83
|
+
constructor(options) {
|
|
84
|
+
this.options = options;
|
|
85
|
+
for (const budget of options.budgets) {
|
|
86
|
+
this.limits.set(budget.tenantId, budget);
|
|
87
|
+
this.spend.set(budget.tenantId, 0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
track(summary) {
|
|
91
|
+
if (!summary.tenantId) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const current = (this.spend.get(summary.tenantId) ?? 0) + summary.total;
|
|
95
|
+
this.spend.set(summary.tenantId, current);
|
|
96
|
+
const budget = this.limits.get(summary.tenantId);
|
|
97
|
+
if (!budget) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const threshold = budget.alertThreshold ?? 0.8;
|
|
101
|
+
if (current >= budget.monthlyLimit * threshold) {
|
|
102
|
+
this.options.onAlert?.({
|
|
103
|
+
tenantId: summary.tenantId,
|
|
104
|
+
limit: budget.monthlyLimit,
|
|
105
|
+
total: current,
|
|
106
|
+
summary
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
getSpend(tenantId) {
|
|
111
|
+
return this.spend.get(tenantId) ?? 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// src/optimization-recommender.ts
|
|
115
|
+
class OptimizationRecommender {
|
|
116
|
+
generate(summary) {
|
|
117
|
+
const suggestions = [];
|
|
118
|
+
const avgCost = summary.total / summary.samples;
|
|
119
|
+
if (summary.breakdown.dbReads / summary.samples > 1000) {
|
|
120
|
+
suggestions.push({
|
|
121
|
+
operation: summary.operation,
|
|
122
|
+
tenantId: summary.tenantId,
|
|
123
|
+
category: "n_plus_one",
|
|
124
|
+
message: "High average DB read count detected. Consider batching queries or adding pagination.",
|
|
125
|
+
evidence: { avgReads: summary.breakdown.dbReads / summary.samples }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (summary.breakdown.compute / summary.total > 0.6) {
|
|
129
|
+
suggestions.push({
|
|
130
|
+
operation: summary.operation,
|
|
131
|
+
tenantId: summary.tenantId,
|
|
132
|
+
category: "batching",
|
|
133
|
+
message: "Compute dominates cost. Investigate hot loops or move heavy logic to background jobs.",
|
|
134
|
+
evidence: { computeShare: summary.breakdown.compute / summary.total }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (summary.breakdown.external > avgCost * 0.5) {
|
|
138
|
+
suggestions.push({
|
|
139
|
+
operation: summary.operation,
|
|
140
|
+
tenantId: summary.tenantId,
|
|
141
|
+
category: "external",
|
|
142
|
+
message: "External provider spend is high. Reuse results or enable caching.",
|
|
143
|
+
evidence: { externalCost: summary.breakdown.external }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (summary.breakdown.memory > summary.breakdown.compute * 1.2) {
|
|
147
|
+
suggestions.push({
|
|
148
|
+
operation: summary.operation,
|
|
149
|
+
tenantId: summary.tenantId,
|
|
150
|
+
category: "caching",
|
|
151
|
+
message: "Memory utilization suggests cached payloads linger. Tune TTL or stream responses.",
|
|
152
|
+
evidence: { memoryCost: summary.breakdown.memory }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return suggestions;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
defaultCostModel,
|
|
160
|
+
calculateSampleCost,
|
|
161
|
+
OptimizationRecommender,
|
|
162
|
+
CostTracker,
|
|
163
|
+
BudgetAlertManager
|
|
164
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/cost-model.ts
|
|
2
|
+
var defaultCostModel = {
|
|
3
|
+
dbReadCost: 0.000002,
|
|
4
|
+
dbWriteCost: 0.00001,
|
|
5
|
+
computeMsCost: 0.00000015,
|
|
6
|
+
memoryMbMsCost: 0.00000002
|
|
7
|
+
};
|
|
8
|
+
function calculateSampleCost(sample, model) {
|
|
9
|
+
const external = (sample.externalCalls ?? []).reduce((sum, call) => sum + (call.cost ?? 0), 0);
|
|
10
|
+
return {
|
|
11
|
+
dbReads: (sample.dbReads ?? 0) * model.dbReadCost,
|
|
12
|
+
dbWrites: (sample.dbWrites ?? 0) * model.dbWriteCost,
|
|
13
|
+
compute: (sample.computeMs ?? 0) * model.computeMsCost,
|
|
14
|
+
memory: (sample.memoryMbMs ?? 0) * model.memoryMbMsCost,
|
|
15
|
+
external,
|
|
16
|
+
custom: sample.customCost ?? 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// src/cost-tracker.ts
|
|
20
|
+
class CostTracker {
|
|
21
|
+
options;
|
|
22
|
+
totals = new Map;
|
|
23
|
+
costModel;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.costModel = options.costModel ?? defaultCostModel;
|
|
27
|
+
}
|
|
28
|
+
recordSample(sample) {
|
|
29
|
+
const breakdown = calculateSampleCost(sample, this.costModel);
|
|
30
|
+
const total = breakdown.dbReads + breakdown.dbWrites + breakdown.compute + breakdown.memory + breakdown.external + breakdown.custom;
|
|
31
|
+
const key = this.buildKey(sample.operation, sample.tenantId);
|
|
32
|
+
const existing = this.totals.get(key);
|
|
33
|
+
const summary = existing ? {
|
|
34
|
+
...existing,
|
|
35
|
+
total: existing.total + total,
|
|
36
|
+
breakdown: {
|
|
37
|
+
dbReads: existing.breakdown.dbReads + breakdown.dbReads,
|
|
38
|
+
dbWrites: existing.breakdown.dbWrites + breakdown.dbWrites,
|
|
39
|
+
compute: existing.breakdown.compute + breakdown.compute,
|
|
40
|
+
memory: existing.breakdown.memory + breakdown.memory,
|
|
41
|
+
external: existing.breakdown.external + breakdown.external,
|
|
42
|
+
custom: existing.breakdown.custom + breakdown.custom
|
|
43
|
+
},
|
|
44
|
+
samples: existing.samples + 1
|
|
45
|
+
} : {
|
|
46
|
+
operation: sample.operation,
|
|
47
|
+
tenantId: sample.tenantId,
|
|
48
|
+
total,
|
|
49
|
+
breakdown: {
|
|
50
|
+
dbReads: breakdown.dbReads,
|
|
51
|
+
dbWrites: breakdown.dbWrites,
|
|
52
|
+
compute: breakdown.compute,
|
|
53
|
+
memory: breakdown.memory,
|
|
54
|
+
external: breakdown.external,
|
|
55
|
+
custom: breakdown.custom
|
|
56
|
+
},
|
|
57
|
+
samples: 1
|
|
58
|
+
};
|
|
59
|
+
this.totals.set(key, summary);
|
|
60
|
+
this.options.onSampleRecorded?.(sample, total);
|
|
61
|
+
return summary;
|
|
62
|
+
}
|
|
63
|
+
getTotals(filter) {
|
|
64
|
+
const items = Array.from(this.totals.values());
|
|
65
|
+
if (!filter?.tenantId) {
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
return items.filter((item) => item.tenantId === filter.tenantId);
|
|
69
|
+
}
|
|
70
|
+
reset() {
|
|
71
|
+
this.totals.clear();
|
|
72
|
+
}
|
|
73
|
+
buildKey(operation, tenantId) {
|
|
74
|
+
return tenantId ? `${tenantId}:${operation}` : operation;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// src/budget-alert-manager.ts
|
|
78
|
+
class BudgetAlertManager {
|
|
79
|
+
options;
|
|
80
|
+
limits = new Map;
|
|
81
|
+
spend = new Map;
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.options = options;
|
|
84
|
+
for (const budget of options.budgets) {
|
|
85
|
+
this.limits.set(budget.tenantId, budget);
|
|
86
|
+
this.spend.set(budget.tenantId, 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
track(summary) {
|
|
90
|
+
if (!summary.tenantId) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const current = (this.spend.get(summary.tenantId) ?? 0) + summary.total;
|
|
94
|
+
this.spend.set(summary.tenantId, current);
|
|
95
|
+
const budget = this.limits.get(summary.tenantId);
|
|
96
|
+
if (!budget) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const threshold = budget.alertThreshold ?? 0.8;
|
|
100
|
+
if (current >= budget.monthlyLimit * threshold) {
|
|
101
|
+
this.options.onAlert?.({
|
|
102
|
+
tenantId: summary.tenantId,
|
|
103
|
+
limit: budget.monthlyLimit,
|
|
104
|
+
total: current,
|
|
105
|
+
summary
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
getSpend(tenantId) {
|
|
110
|
+
return this.spend.get(tenantId) ?? 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// src/optimization-recommender.ts
|
|
114
|
+
class OptimizationRecommender {
|
|
115
|
+
generate(summary) {
|
|
116
|
+
const suggestions = [];
|
|
117
|
+
const avgCost = summary.total / summary.samples;
|
|
118
|
+
if (summary.breakdown.dbReads / summary.samples > 1000) {
|
|
119
|
+
suggestions.push({
|
|
120
|
+
operation: summary.operation,
|
|
121
|
+
tenantId: summary.tenantId,
|
|
122
|
+
category: "n_plus_one",
|
|
123
|
+
message: "High average DB read count detected. Consider batching queries or adding pagination.",
|
|
124
|
+
evidence: { avgReads: summary.breakdown.dbReads / summary.samples }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (summary.breakdown.compute / summary.total > 0.6) {
|
|
128
|
+
suggestions.push({
|
|
129
|
+
operation: summary.operation,
|
|
130
|
+
tenantId: summary.tenantId,
|
|
131
|
+
category: "batching",
|
|
132
|
+
message: "Compute dominates cost. Investigate hot loops or move heavy logic to background jobs.",
|
|
133
|
+
evidence: { computeShare: summary.breakdown.compute / summary.total }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (summary.breakdown.external > avgCost * 0.5) {
|
|
137
|
+
suggestions.push({
|
|
138
|
+
operation: summary.operation,
|
|
139
|
+
tenantId: summary.tenantId,
|
|
140
|
+
category: "external",
|
|
141
|
+
message: "External provider spend is high. Reuse results or enable caching.",
|
|
142
|
+
evidence: { externalCost: summary.breakdown.external }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (summary.breakdown.memory > summary.breakdown.compute * 1.2) {
|
|
146
|
+
suggestions.push({
|
|
147
|
+
operation: summary.operation,
|
|
148
|
+
tenantId: summary.tenantId,
|
|
149
|
+
category: "caching",
|
|
150
|
+
message: "Memory utilization suggests cached payloads linger. Tune TTL or stream responses.",
|
|
151
|
+
evidence: { memoryCost: summary.breakdown.memory }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return suggestions;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
defaultCostModel,
|
|
159
|
+
calculateSampleCost,
|
|
160
|
+
OptimizationRecommender,
|
|
161
|
+
CostTracker,
|
|
162
|
+
BudgetAlertManager
|
|
163
|
+
};
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { OperationCostSummary, OptimizationSuggestion } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
declare class OptimizationRecommender {
|
|
5
|
-
generate(summary: OperationCostSummary): OptimizationSuggestion[];
|
|
1
|
+
import type { OperationCostSummary, OptimizationSuggestion } from './types';
|
|
2
|
+
export declare class OptimizationRecommender {
|
|
3
|
+
generate(summary: OperationCostSummary): OptimizationSuggestion[];
|
|
6
4
|
}
|
|
7
|
-
//#endregion
|
|
8
|
-
export { OptimizationRecommender };
|
|
9
5
|
//# sourceMappingURL=optimization-recommender.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimization-recommender.d.ts","
|
|
1
|
+
{"version":3,"file":"optimization-recommender.d.ts","sourceRoot":"","sources":["../src/optimization-recommender.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAE5E,qBAAa,uBAAuB;IAClC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,GAAG,sBAAsB,EAAE;CAkDlE"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,63 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
requestCount?: number;
|
|
1
|
+
export interface ExternalCallCost {
|
|
2
|
+
provider: string;
|
|
3
|
+
cost?: number;
|
|
4
|
+
durationMs?: number;
|
|
5
|
+
requestCount?: number;
|
|
7
6
|
}
|
|
8
|
-
interface CostSample {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
export interface CostSample {
|
|
8
|
+
operation: string;
|
|
9
|
+
tenantId?: string;
|
|
10
|
+
environment?: 'dev' | 'staging' | 'prod';
|
|
11
|
+
dbReads?: number;
|
|
12
|
+
dbWrites?: number;
|
|
13
|
+
computeMs?: number;
|
|
14
|
+
memoryMbMs?: number;
|
|
15
|
+
externalCalls?: ExternalCallCost[];
|
|
16
|
+
customCost?: number;
|
|
17
|
+
timestamp?: Date;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
20
19
|
}
|
|
21
|
-
interface CostModel {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
export interface CostModel {
|
|
21
|
+
dbReadCost: number;
|
|
22
|
+
dbWriteCost: number;
|
|
23
|
+
computeMsCost: number;
|
|
24
|
+
memoryMbMsCost: number;
|
|
26
25
|
}
|
|
27
|
-
interface OperationCostSummary {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
export interface OperationCostSummary {
|
|
27
|
+
operation: string;
|
|
28
|
+
tenantId?: string;
|
|
29
|
+
total: number;
|
|
30
|
+
breakdown: {
|
|
31
|
+
dbReads: number;
|
|
32
|
+
dbWrites: number;
|
|
33
|
+
compute: number;
|
|
34
|
+
memory: number;
|
|
35
|
+
external: number;
|
|
36
|
+
custom: number;
|
|
37
|
+
};
|
|
38
|
+
samples: number;
|
|
40
39
|
}
|
|
41
|
-
interface TenantBudget {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
export interface TenantBudget {
|
|
41
|
+
tenantId: string;
|
|
42
|
+
monthlyLimit: number;
|
|
43
|
+
currency?: string;
|
|
44
|
+
alertThreshold?: number;
|
|
45
|
+
currentSpend?: number;
|
|
47
46
|
}
|
|
48
|
-
interface OptimizationSuggestion {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
export interface OptimizationSuggestion {
|
|
48
|
+
operation: string;
|
|
49
|
+
tenantId?: string;
|
|
50
|
+
category: 'n_plus_one' | 'caching' | 'batching' | 'external';
|
|
51
|
+
message: string;
|
|
52
|
+
evidence: Record<string, unknown>;
|
|
54
53
|
}
|
|
55
|
-
interface BudgetAlert {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
export interface BudgetAlert {
|
|
55
|
+
tenantId: string;
|
|
56
|
+
limit: number;
|
|
57
|
+
actual: number;
|
|
58
|
+
triggeredAt: Date;
|
|
60
59
|
}
|
|
61
|
-
//#endregion
|
|
62
|
-
export { BudgetAlert, CostModel, CostSample, ExternalCallCost, OperationCostSummary, OptimizationSuggestion, TenantBudget };
|
|
63
60
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,CAAC;CACnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.cost-tracking",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.0",
|
|
4
4
|
"description": "API cost tracking and budgeting",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -19,31 +19,36 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
21
21
|
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
22
|
-
"build": "bun build:
|
|
23
|
-
"build:bundle": "
|
|
24
|
-
"build:types": "
|
|
25
|
-
"dev": "bun
|
|
22
|
+
"build": "bun run prebuild && bun run build:bundle && bun run build:types",
|
|
23
|
+
"build:bundle": "contractspec-bun-build transpile",
|
|
24
|
+
"build:types": "contractspec-bun-build types",
|
|
25
|
+
"dev": "contractspec-bun-build dev",
|
|
26
26
|
"clean": "rimraf dist .turbo",
|
|
27
27
|
"lint": "bun lint:fix",
|
|
28
28
|
"lint:fix": "eslint src --fix",
|
|
29
29
|
"lint:check": "eslint src",
|
|
30
|
-
"test": "bun test"
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"prebuild": "contractspec-bun-build prebuild",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
|
-
"@contractspec/tool.
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"typescript": "^5.9.3"
|
|
35
|
+
"@contractspec/tool.typescript": "1.59.0",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"@contractspec/tool.bun": "1.58.0"
|
|
37
38
|
},
|
|
38
39
|
"exports": {
|
|
39
|
-
".": "./
|
|
40
|
-
"./*": "./*"
|
|
40
|
+
".": "./src/index.ts"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public",
|
|
44
44
|
"exports": {
|
|
45
|
-
".":
|
|
46
|
-
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"bun": "./dist/index.js",
|
|
48
|
+
"node": "./dist/node/index.mjs",
|
|
49
|
+
"browser": "./dist/browser/index.js",
|
|
50
|
+
"default": "./dist/index.js"
|
|
51
|
+
}
|
|
47
52
|
},
|
|
48
53
|
"registry": "https://registry.npmjs.org/"
|
|
49
54
|
},
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
//#region src/budget-alert-manager.ts
|
|
2
|
-
var BudgetAlertManager = class {
|
|
3
|
-
limits = /* @__PURE__ */ new Map();
|
|
4
|
-
spend = /* @__PURE__ */ new Map();
|
|
5
|
-
constructor(options) {
|
|
6
|
-
this.options = options;
|
|
7
|
-
for (const budget of options.budgets) {
|
|
8
|
-
this.limits.set(budget.tenantId, budget);
|
|
9
|
-
this.spend.set(budget.tenantId, 0);
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
track(summary) {
|
|
13
|
-
if (!summary.tenantId) return;
|
|
14
|
-
const current = (this.spend.get(summary.tenantId) ?? 0) + summary.total;
|
|
15
|
-
this.spend.set(summary.tenantId, current);
|
|
16
|
-
const budget = this.limits.get(summary.tenantId);
|
|
17
|
-
if (!budget) return;
|
|
18
|
-
const threshold = budget.alertThreshold ?? .8;
|
|
19
|
-
if (current >= budget.monthlyLimit * threshold) this.options.onAlert?.({
|
|
20
|
-
tenantId: summary.tenantId,
|
|
21
|
-
limit: budget.monthlyLimit,
|
|
22
|
-
total: current,
|
|
23
|
-
summary
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
getSpend(tenantId) {
|
|
27
|
-
return this.spend.get(tenantId) ?? 0;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
export { BudgetAlertManager };
|
|
33
|
-
//# sourceMappingURL=budget-alert-manager.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"budget-alert-manager.js","names":[],"sources":["../src/budget-alert-manager.ts"],"sourcesContent":["import type { OperationCostSummary, TenantBudget } from './types';\n\nexport interface BudgetAlertManagerOptions {\n budgets: TenantBudget[];\n onAlert?: (payload: {\n tenantId: string;\n limit: number;\n total: number;\n summary: OperationCostSummary;\n }) => void;\n}\n\nexport class BudgetAlertManager {\n private readonly limits = new Map<string, TenantBudget>();\n private readonly spend = new Map<string, number>();\n\n constructor(private readonly options: BudgetAlertManagerOptions) {\n for (const budget of options.budgets) {\n this.limits.set(budget.tenantId, budget);\n this.spend.set(budget.tenantId, 0);\n }\n }\n\n track(summary: OperationCostSummary) {\n if (!summary.tenantId) {\n return;\n }\n\n const current = (this.spend.get(summary.tenantId) ?? 0) + summary.total;\n this.spend.set(summary.tenantId, current);\n\n const budget = this.limits.get(summary.tenantId);\n if (!budget) {\n return;\n }\n\n const threshold = budget.alertThreshold ?? 0.8;\n if (current >= budget.monthlyLimit * threshold) {\n this.options.onAlert?.({\n tenantId: summary.tenantId,\n limit: budget.monthlyLimit,\n total: current,\n summary,\n });\n }\n }\n\n getSpend(tenantId: string) {\n return this.spend.get(tenantId) ?? 0;\n }\n}\n"],"mappings":";AAYA,IAAa,qBAAb,MAAgC;CAC9B,AAAiB,yBAAS,IAAI,KAA2B;CACzD,AAAiB,wBAAQ,IAAI,KAAqB;CAElD,YAAY,AAAiB,SAAoC;EAApC;AAC3B,OAAK,MAAM,UAAU,QAAQ,SAAS;AACpC,QAAK,OAAO,IAAI,OAAO,UAAU,OAAO;AACxC,QAAK,MAAM,IAAI,OAAO,UAAU,EAAE;;;CAItC,MAAM,SAA+B;AACnC,MAAI,CAAC,QAAQ,SACX;EAGF,MAAM,WAAW,KAAK,MAAM,IAAI,QAAQ,SAAS,IAAI,KAAK,QAAQ;AAClE,OAAK,MAAM,IAAI,QAAQ,UAAU,QAAQ;EAEzC,MAAM,SAAS,KAAK,OAAO,IAAI,QAAQ,SAAS;AAChD,MAAI,CAAC,OACH;EAGF,MAAM,YAAY,OAAO,kBAAkB;AAC3C,MAAI,WAAW,OAAO,eAAe,UACnC,MAAK,QAAQ,UAAU;GACrB,UAAU,QAAQ;GAClB,OAAO,OAAO;GACd,OAAO;GACP;GACD,CAAC;;CAIN,SAAS,UAAkB;AACzB,SAAO,KAAK,MAAM,IAAI,SAAS,IAAI"}
|
package/dist/cost-model.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//#region src/cost-model.ts
|
|
2
|
-
const defaultCostModel = {
|
|
3
|
-
dbReadCost: 2e-6,
|
|
4
|
-
dbWriteCost: 1e-5,
|
|
5
|
-
computeMsCost: 15e-8,
|
|
6
|
-
memoryMbMsCost: 2e-8
|
|
7
|
-
};
|
|
8
|
-
function calculateSampleCost(sample, model) {
|
|
9
|
-
const external = (sample.externalCalls ?? []).reduce((sum, call) => sum + (call.cost ?? 0), 0);
|
|
10
|
-
return {
|
|
11
|
-
dbReads: (sample.dbReads ?? 0) * model.dbReadCost,
|
|
12
|
-
dbWrites: (sample.dbWrites ?? 0) * model.dbWriteCost,
|
|
13
|
-
compute: (sample.computeMs ?? 0) * model.computeMsCost,
|
|
14
|
-
memory: (sample.memoryMbMs ?? 0) * model.memoryMbMsCost,
|
|
15
|
-
external,
|
|
16
|
-
custom: sample.customCost ?? 0
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
export { calculateSampleCost, defaultCostModel };
|
|
22
|
-
//# sourceMappingURL=cost-model.js.map
|
package/dist/cost-model.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cost-model.js","names":[],"sources":["../src/cost-model.ts"],"sourcesContent":["import type { CostModel, CostSample } from './types';\n\nexport const defaultCostModel: CostModel = {\n dbReadCost: 0.000002,\n dbWriteCost: 0.00001,\n computeMsCost: 0.00000015,\n memoryMbMsCost: 0.00000002,\n};\n\nexport function calculateSampleCost(sample: CostSample, model: CostModel) {\n const external = (sample.externalCalls ?? []).reduce(\n (sum, call) => sum + (call.cost ?? 0),\n 0\n );\n\n return {\n dbReads: (sample.dbReads ?? 0) * model.dbReadCost,\n dbWrites: (sample.dbWrites ?? 0) * model.dbWriteCost,\n compute: (sample.computeMs ?? 0) * model.computeMsCost,\n memory: (sample.memoryMbMs ?? 0) * model.memoryMbMsCost,\n external,\n custom: sample.customCost ?? 0,\n };\n}\n"],"mappings":";AAEA,MAAa,mBAA8B;CACzC,YAAY;CACZ,aAAa;CACb,eAAe;CACf,gBAAgB;CACjB;AAED,SAAgB,oBAAoB,QAAoB,OAAkB;CACxE,MAAM,YAAY,OAAO,iBAAiB,EAAE,EAAE,QAC3C,KAAK,SAAS,OAAO,KAAK,QAAQ,IACnC,EACD;AAED,QAAO;EACL,UAAU,OAAO,WAAW,KAAK,MAAM;EACvC,WAAW,OAAO,YAAY,KAAK,MAAM;EACzC,UAAU,OAAO,aAAa,KAAK,MAAM;EACzC,SAAS,OAAO,cAAc,KAAK,MAAM;EACzC;EACA,QAAQ,OAAO,cAAc;EAC9B"}
|
package/dist/cost-tracker.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { calculateSampleCost, defaultCostModel } from "./cost-model.js";
|
|
2
|
-
|
|
3
|
-
//#region src/cost-tracker.ts
|
|
4
|
-
var CostTracker = class {
|
|
5
|
-
totals = /* @__PURE__ */ new Map();
|
|
6
|
-
costModel;
|
|
7
|
-
constructor(options = {}) {
|
|
8
|
-
this.options = options;
|
|
9
|
-
this.costModel = options.costModel ?? defaultCostModel;
|
|
10
|
-
}
|
|
11
|
-
recordSample(sample) {
|
|
12
|
-
const breakdown = calculateSampleCost(sample, this.costModel);
|
|
13
|
-
const total = breakdown.dbReads + breakdown.dbWrites + breakdown.compute + breakdown.memory + breakdown.external + breakdown.custom;
|
|
14
|
-
const key = this.buildKey(sample.operation, sample.tenantId);
|
|
15
|
-
const existing = this.totals.get(key);
|
|
16
|
-
const summary = existing ? {
|
|
17
|
-
...existing,
|
|
18
|
-
total: existing.total + total,
|
|
19
|
-
breakdown: {
|
|
20
|
-
dbReads: existing.breakdown.dbReads + breakdown.dbReads,
|
|
21
|
-
dbWrites: existing.breakdown.dbWrites + breakdown.dbWrites,
|
|
22
|
-
compute: existing.breakdown.compute + breakdown.compute,
|
|
23
|
-
memory: existing.breakdown.memory + breakdown.memory,
|
|
24
|
-
external: existing.breakdown.external + breakdown.external,
|
|
25
|
-
custom: existing.breakdown.custom + breakdown.custom
|
|
26
|
-
},
|
|
27
|
-
samples: existing.samples + 1
|
|
28
|
-
} : {
|
|
29
|
-
operation: sample.operation,
|
|
30
|
-
tenantId: sample.tenantId,
|
|
31
|
-
total,
|
|
32
|
-
breakdown: {
|
|
33
|
-
dbReads: breakdown.dbReads,
|
|
34
|
-
dbWrites: breakdown.dbWrites,
|
|
35
|
-
compute: breakdown.compute,
|
|
36
|
-
memory: breakdown.memory,
|
|
37
|
-
external: breakdown.external,
|
|
38
|
-
custom: breakdown.custom
|
|
39
|
-
},
|
|
40
|
-
samples: 1
|
|
41
|
-
};
|
|
42
|
-
this.totals.set(key, summary);
|
|
43
|
-
this.options.onSampleRecorded?.(sample, total);
|
|
44
|
-
return summary;
|
|
45
|
-
}
|
|
46
|
-
getTotals(filter) {
|
|
47
|
-
const items = Array.from(this.totals.values());
|
|
48
|
-
if (!filter?.tenantId) return items;
|
|
49
|
-
return items.filter((item) => item.tenantId === filter.tenantId);
|
|
50
|
-
}
|
|
51
|
-
reset() {
|
|
52
|
-
this.totals.clear();
|
|
53
|
-
}
|
|
54
|
-
buildKey(operation, tenantId) {
|
|
55
|
-
return tenantId ? `${tenantId}:${operation}` : operation;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
//#endregion
|
|
60
|
-
export { CostTracker };
|
|
61
|
-
//# sourceMappingURL=cost-tracker.js.map
|
package/dist/cost-tracker.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cost-tracker.js","names":[],"sources":["../src/cost-tracker.ts"],"sourcesContent":["import { calculateSampleCost, defaultCostModel } from './cost-model';\nimport type { CostModel, CostSample, OperationCostSummary } from './types';\n\nexport interface CostTrackerOptions {\n costModel?: CostModel;\n onSampleRecorded?: (sample: CostSample, total: number) => void;\n}\n\nexport class CostTracker {\n private readonly totals = new Map<string, OperationCostSummary>();\n private readonly costModel: CostModel;\n\n constructor(private readonly options: CostTrackerOptions = {}) {\n this.costModel = options.costModel ?? defaultCostModel;\n }\n\n recordSample(sample: CostSample): OperationCostSummary {\n const breakdown = calculateSampleCost(sample, this.costModel);\n const total =\n breakdown.dbReads +\n breakdown.dbWrites +\n breakdown.compute +\n breakdown.memory +\n breakdown.external +\n breakdown.custom;\n\n const key = this.buildKey(sample.operation, sample.tenantId);\n const existing = this.totals.get(key);\n\n const summary: OperationCostSummary = existing\n ? {\n ...existing,\n total: existing.total + total,\n breakdown: {\n dbReads: existing.breakdown.dbReads + breakdown.dbReads,\n dbWrites: existing.breakdown.dbWrites + breakdown.dbWrites,\n compute: existing.breakdown.compute + breakdown.compute,\n memory: existing.breakdown.memory + breakdown.memory,\n external: existing.breakdown.external + breakdown.external,\n custom: existing.breakdown.custom + breakdown.custom,\n },\n samples: existing.samples + 1,\n }\n : {\n operation: sample.operation,\n tenantId: sample.tenantId,\n total,\n breakdown: {\n dbReads: breakdown.dbReads,\n dbWrites: breakdown.dbWrites,\n compute: breakdown.compute,\n memory: breakdown.memory,\n external: breakdown.external,\n custom: breakdown.custom,\n },\n samples: 1,\n };\n\n this.totals.set(key, summary);\n this.options.onSampleRecorded?.(sample, total);\n return summary;\n }\n\n getTotals(filter?: { tenantId?: string }) {\n const items = Array.from(this.totals.values());\n if (!filter?.tenantId) {\n return items;\n }\n return items.filter((item) => item.tenantId === filter.tenantId);\n }\n\n reset() {\n this.totals.clear();\n }\n\n private buildKey(operation: string, tenantId?: string) {\n return tenantId ? `${tenantId}:${operation}` : operation;\n }\n}\n"],"mappings":";;;AAQA,IAAa,cAAb,MAAyB;CACvB,AAAiB,yBAAS,IAAI,KAAmC;CACjE,AAAiB;CAEjB,YAAY,AAAiB,UAA8B,EAAE,EAAE;EAAlC;AAC3B,OAAK,YAAY,QAAQ,aAAa;;CAGxC,aAAa,QAA0C;EACrD,MAAM,YAAY,oBAAoB,QAAQ,KAAK,UAAU;EAC7D,MAAM,QACJ,UAAU,UACV,UAAU,WACV,UAAU,UACV,UAAU,SACV,UAAU,WACV,UAAU;EAEZ,MAAM,MAAM,KAAK,SAAS,OAAO,WAAW,OAAO,SAAS;EAC5D,MAAM,WAAW,KAAK,OAAO,IAAI,IAAI;EAErC,MAAM,UAAgC,WAClC;GACE,GAAG;GACH,OAAO,SAAS,QAAQ;GACxB,WAAW;IACT,SAAS,SAAS,UAAU,UAAU,UAAU;IAChD,UAAU,SAAS,UAAU,WAAW,UAAU;IAClD,SAAS,SAAS,UAAU,UAAU,UAAU;IAChD,QAAQ,SAAS,UAAU,SAAS,UAAU;IAC9C,UAAU,SAAS,UAAU,WAAW,UAAU;IAClD,QAAQ,SAAS,UAAU,SAAS,UAAU;IAC/C;GACD,SAAS,SAAS,UAAU;GAC7B,GACD;GACE,WAAW,OAAO;GAClB,UAAU,OAAO;GACjB;GACA,WAAW;IACT,SAAS,UAAU;IACnB,UAAU,UAAU;IACpB,SAAS,UAAU;IACnB,QAAQ,UAAU;IAClB,UAAU,UAAU;IACpB,QAAQ,UAAU;IACnB;GACD,SAAS;GACV;AAEL,OAAK,OAAO,IAAI,KAAK,QAAQ;AAC7B,OAAK,QAAQ,mBAAmB,QAAQ,MAAM;AAC9C,SAAO;;CAGT,UAAU,QAAgC;EACxC,MAAM,QAAQ,MAAM,KAAK,KAAK,OAAO,QAAQ,CAAC;AAC9C,MAAI,CAAC,QAAQ,SACX,QAAO;AAET,SAAO,MAAM,QAAQ,SAAS,KAAK,aAAa,OAAO,SAAS;;CAGlE,QAAQ;AACN,OAAK,OAAO,OAAO;;CAGrB,AAAQ,SAAS,WAAmB,UAAmB;AACrD,SAAO,WAAW,GAAG,SAAS,GAAG,cAAc"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
//#region src/optimization-recommender.ts
|
|
2
|
-
var OptimizationRecommender = class {
|
|
3
|
-
generate(summary) {
|
|
4
|
-
const suggestions = [];
|
|
5
|
-
const avgCost = summary.total / summary.samples;
|
|
6
|
-
if (summary.breakdown.dbReads / summary.samples > 1e3) suggestions.push({
|
|
7
|
-
operation: summary.operation,
|
|
8
|
-
tenantId: summary.tenantId,
|
|
9
|
-
category: "n_plus_one",
|
|
10
|
-
message: "High average DB read count detected. Consider batching queries or adding pagination.",
|
|
11
|
-
evidence: { avgReads: summary.breakdown.dbReads / summary.samples }
|
|
12
|
-
});
|
|
13
|
-
if (summary.breakdown.compute / summary.total > .6) suggestions.push({
|
|
14
|
-
operation: summary.operation,
|
|
15
|
-
tenantId: summary.tenantId,
|
|
16
|
-
category: "batching",
|
|
17
|
-
message: "Compute dominates cost. Investigate hot loops or move heavy logic to background jobs.",
|
|
18
|
-
evidence: { computeShare: summary.breakdown.compute / summary.total }
|
|
19
|
-
});
|
|
20
|
-
if (summary.breakdown.external > avgCost * .5) suggestions.push({
|
|
21
|
-
operation: summary.operation,
|
|
22
|
-
tenantId: summary.tenantId,
|
|
23
|
-
category: "external",
|
|
24
|
-
message: "External provider spend is high. Reuse results or enable caching.",
|
|
25
|
-
evidence: { externalCost: summary.breakdown.external }
|
|
26
|
-
});
|
|
27
|
-
if (summary.breakdown.memory > summary.breakdown.compute * 1.2) suggestions.push({
|
|
28
|
-
operation: summary.operation,
|
|
29
|
-
tenantId: summary.tenantId,
|
|
30
|
-
category: "caching",
|
|
31
|
-
message: "Memory utilization suggests cached payloads linger. Tune TTL or stream responses.",
|
|
32
|
-
evidence: { memoryCost: summary.breakdown.memory }
|
|
33
|
-
});
|
|
34
|
-
return suggestions;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
//#endregion
|
|
39
|
-
export { OptimizationRecommender };
|
|
40
|
-
//# sourceMappingURL=optimization-recommender.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"optimization-recommender.js","names":[],"sources":["../src/optimization-recommender.ts"],"sourcesContent":["import type { OperationCostSummary, OptimizationSuggestion } from './types';\n\nexport class OptimizationRecommender {\n generate(summary: OperationCostSummary): OptimizationSuggestion[] {\n const suggestions: OptimizationSuggestion[] = [];\n const avgCost = summary.total / summary.samples;\n\n if (summary.breakdown.dbReads / summary.samples > 1000) {\n suggestions.push({\n operation: summary.operation,\n tenantId: summary.tenantId,\n category: 'n_plus_one',\n message:\n 'High average DB read count detected. Consider batching queries or adding pagination.',\n evidence: { avgReads: summary.breakdown.dbReads / summary.samples },\n });\n }\n\n if (summary.breakdown.compute / summary.total > 0.6) {\n suggestions.push({\n operation: summary.operation,\n tenantId: summary.tenantId,\n category: 'batching',\n message:\n 'Compute dominates cost. Investigate hot loops or move heavy logic to background jobs.',\n evidence: { computeShare: summary.breakdown.compute / summary.total },\n });\n }\n\n if (summary.breakdown.external > avgCost * 0.5) {\n suggestions.push({\n operation: summary.operation,\n tenantId: summary.tenantId,\n category: 'external',\n message:\n 'External provider spend is high. Reuse results or enable caching.',\n evidence: { externalCost: summary.breakdown.external },\n });\n }\n\n if (summary.breakdown.memory > summary.breakdown.compute * 1.2) {\n suggestions.push({\n operation: summary.operation,\n tenantId: summary.tenantId,\n category: 'caching',\n message:\n 'Memory utilization suggests cached payloads linger. Tune TTL or stream responses.',\n evidence: { memoryCost: summary.breakdown.memory },\n });\n }\n\n return suggestions;\n }\n}\n"],"mappings":";AAEA,IAAa,0BAAb,MAAqC;CACnC,SAAS,SAAyD;EAChE,MAAM,cAAwC,EAAE;EAChD,MAAM,UAAU,QAAQ,QAAQ,QAAQ;AAExC,MAAI,QAAQ,UAAU,UAAU,QAAQ,UAAU,IAChD,aAAY,KAAK;GACf,WAAW,QAAQ;GACnB,UAAU,QAAQ;GAClB,UAAU;GACV,SACE;GACF,UAAU,EAAE,UAAU,QAAQ,UAAU,UAAU,QAAQ,SAAS;GACpE,CAAC;AAGJ,MAAI,QAAQ,UAAU,UAAU,QAAQ,QAAQ,GAC9C,aAAY,KAAK;GACf,WAAW,QAAQ;GACnB,UAAU,QAAQ;GAClB,UAAU;GACV,SACE;GACF,UAAU,EAAE,cAAc,QAAQ,UAAU,UAAU,QAAQ,OAAO;GACtE,CAAC;AAGJ,MAAI,QAAQ,UAAU,WAAW,UAAU,GACzC,aAAY,KAAK;GACf,WAAW,QAAQ;GACnB,UAAU,QAAQ;GAClB,UAAU;GACV,SACE;GACF,UAAU,EAAE,cAAc,QAAQ,UAAU,UAAU;GACvD,CAAC;AAGJ,MAAI,QAAQ,UAAU,SAAS,QAAQ,UAAU,UAAU,IACzD,aAAY,KAAK;GACf,WAAW,QAAQ;GACnB,UAAU,QAAQ;GAClB,UAAU;GACV,SACE;GACF,UAAU,EAAE,YAAY,QAAQ,UAAU,QAAQ;GACnD,CAAC;AAGJ,SAAO"}
|