cloudbroker-sdk 0.2.0 → 0.4.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/README.md +29 -0
- package/package.json +1 -1
- package/src/client.js +2 -2
- package/src/cloud-wrapper.js +38 -5
- package/src/context.js +14 -0
- package/src/test.js +20 -0
package/README.md
CHANGED
|
@@ -146,6 +146,35 @@ Cada chamada gera um evento `POST /api/cloud/events` com `service`, `operation`,
|
|
|
146
146
|
`product`, `team`, `label`, `requests`, `bytesIn`/`bytesOut`, `costUSD` estimado e
|
|
147
147
|
`latencyMs`. O `cbLabel` nunca é repassado ao AWS SDK.
|
|
148
148
|
|
|
149
|
+
### Atribuição automática por requisição (Express)
|
|
150
|
+
|
|
151
|
+
Em vez de passar `cbLabel` em cada `.send()`, use o middleware: todas as chamadas
|
|
152
|
+
de nuvem feitas durante a requisição herdam um label de negócio automaticamente
|
|
153
|
+
(via `AsyncLocalStorage`). Ótimo pra agrupar custo por **módulo** ou **rota**.
|
|
154
|
+
|
|
155
|
+
Cada campo (`label`, `product`, `team`) pode ser fixo (string) ou uma função
|
|
156
|
+
`(req) => string`. Isso permite a hierarquia **Grupo (team) → App (product) →
|
|
157
|
+
Trecho (label) → Serviço** na visão de custo.
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
import express from "express";
|
|
161
|
+
import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
162
|
+
|
|
163
|
+
const app = express();
|
|
164
|
+
|
|
165
|
+
app.use(CloudBrokerCloud.expressMiddleware({
|
|
166
|
+
team: (req) => (req.path.startsWith("/api/admin") ? "Plataforma" : "Produto"), // Grupo
|
|
167
|
+
product: (req) => req.path.split("/")[2] || "core", // App
|
|
168
|
+
label: (req) => `${req.method} ${req.path}`, // Trecho
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// daqui pra frente, qualquer s3.send(...)/ddb.send(...) dentro da request
|
|
172
|
+
// é atribuído ao grupo/app/trecho — sem cbLabel manual.
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Prioridade de cada campo: explícito no `.send()` (`cbLabel`/`cbProduct`/`cbTeam`) >
|
|
176
|
+
contexto do middleware > valor estático do `wrapAWS` > `null`.
|
|
177
|
+
|
|
149
178
|
## Garantias
|
|
150
179
|
|
|
151
180
|
- **Fire-and-forget**: se o CloudBroker estiver fora, sua aplicação não é afetada
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -37,7 +37,7 @@ export class CloudBrokerClient {
|
|
|
37
37
|
try {
|
|
38
38
|
const payload = {
|
|
39
39
|
...event,
|
|
40
|
-
sdkVersion: "0.
|
|
40
|
+
sdkVersion: "0.4.0",
|
|
41
41
|
sentAt: new Date().toISOString(),
|
|
42
42
|
};
|
|
43
43
|
if (this.#cloudQueue.length < MAX_QUEUE_SIZE) {
|
|
@@ -93,7 +93,7 @@ export class CloudBrokerClient {
|
|
|
93
93
|
try {
|
|
94
94
|
const payload = {
|
|
95
95
|
...event,
|
|
96
|
-
sdkVersion: "0.
|
|
96
|
+
sdkVersion: "0.4.0",
|
|
97
97
|
sentAt: new Date().toISOString(),
|
|
98
98
|
};
|
|
99
99
|
|
package/src/cloud-wrapper.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { CloudBrokerClient } from "./client.js";
|
|
26
26
|
import { estimateCloudCost } from "./pricing-cloud.js";
|
|
27
|
+
import { cbStore } from "./context.js";
|
|
27
28
|
|
|
28
29
|
export class CloudBrokerCloud {
|
|
29
30
|
/**
|
|
@@ -53,11 +54,16 @@ export class CloudBrokerCloud {
|
|
|
53
54
|
client.send = async (command, sendOptions) => {
|
|
54
55
|
const start = Date.now();
|
|
55
56
|
const operation = (command?.constructor?.name || "").replace(/Command$/, "");
|
|
56
|
-
|
|
57
|
+
// Prioridade: explícito no .send() > contexto da requisição (expressMiddleware)
|
|
58
|
+
// > valor estático do wrapAWS. Permite Grupo/App/Trecho por requisição.
|
|
59
|
+
const ctx = cbStore.getStore() || {};
|
|
60
|
+
const cbLabel = sendOptions?.cbLabel || ctx.label || null;
|
|
61
|
+
const evProduct = sendOptions?.cbProduct || ctx.product || product || null;
|
|
62
|
+
const evTeam = sendOptions?.cbTeam || ctx.team || team || null;
|
|
57
63
|
const bytesIn = CloudBrokerCloud.#inputBytes(command?.input);
|
|
58
64
|
|
|
59
|
-
// Passa adiante apenas as options nativas — nunca vaza
|
|
60
|
-
const { cbLabel:
|
|
65
|
+
// Passa adiante apenas as options nativas — nunca vaza campos cb* pro AWS SDK.
|
|
66
|
+
const { cbLabel: _l, cbProduct: _p, cbTeam: _t, ...awsOptions } = sendOptions || {};
|
|
61
67
|
const result = await originalSend(command, Object.keys(awsOptions).length ? awsOptions : undefined);
|
|
62
68
|
|
|
63
69
|
try {
|
|
@@ -70,8 +76,8 @@ export class CloudBrokerCloud {
|
|
|
70
76
|
provider: "aws",
|
|
71
77
|
service,
|
|
72
78
|
operation,
|
|
73
|
-
product:
|
|
74
|
-
team:
|
|
79
|
+
product: evProduct,
|
|
80
|
+
team: evTeam,
|
|
75
81
|
label: cbLabel,
|
|
76
82
|
requests: 1,
|
|
77
83
|
bytesIn,
|
|
@@ -91,6 +97,33 @@ export class CloudBrokerCloud {
|
|
|
91
97
|
return client;
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Middleware Express que atribui contexto de negócio a todas as chamadas de
|
|
102
|
+
* nuvem feitas durante a requisição (via AsyncLocalStorage). Cada campo pode ser
|
|
103
|
+
* fixo (string) ou uma função (req) => string — ex: mapear rota → módulo/app/grupo.
|
|
104
|
+
* Sem precisar passar cbLabel/cbProduct/cbTeam manual em cada send.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} [options]
|
|
107
|
+
* @param {string|((req)=>string)} [options.label] — trecho (default: "MÉTODO /caminho")
|
|
108
|
+
* @param {string|((req)=>string)} [options.product] — App (sobrescreve o product do wrapAWS)
|
|
109
|
+
* @param {string|((req)=>string)} [options.team] — Grupo (sobrescreve o team do wrapAWS)
|
|
110
|
+
* @returns {(req,res,next)=>void}
|
|
111
|
+
*/
|
|
112
|
+
static expressMiddleware(options = {}) {
|
|
113
|
+
const mk = (opt, fallback) =>
|
|
114
|
+
typeof opt === "function" ? opt :
|
|
115
|
+
typeof opt === "string" ? () => opt :
|
|
116
|
+
fallback;
|
|
117
|
+
const resolveLabel = mk(options.label, (req) => `${req.method || ""} ${req.baseUrl || ""}${req.route?.path || req.path || ""}`.trim());
|
|
118
|
+
const resolveProduct = mk(options.product, () => undefined);
|
|
119
|
+
const resolveTeam = mk(options.team, () => undefined);
|
|
120
|
+
|
|
121
|
+
return (req, res, next) => {
|
|
122
|
+
const safe = (fn) => { try { return fn(req); } catch { return undefined; } };
|
|
123
|
+
cbStore.run({ label: safe(resolveLabel), product: safe(resolveProduct), team: safe(resolveTeam) }, () => next());
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
/** Detecta o serviço AWS pelo nome do construtor do client. */
|
|
95
128
|
static #detectService(client) {
|
|
96
129
|
const name = client?.constructor?.name || "";
|
package/src/context.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contexto de requisição via AsyncLocalStorage.
|
|
3
|
+
* Permite atribuir um label de negócio (módulo / rota) a todas as chamadas de
|
|
4
|
+
* nuvem disparadas dentro de uma requisição, sem precisar passar cbLabel manual
|
|
5
|
+
* em cada `.send()`. Server-side apenas (Node).
|
|
6
|
+
*/
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
|
|
9
|
+
export const cbStore = new AsyncLocalStorage();
|
|
10
|
+
|
|
11
|
+
/** Label de negócio do contexto atual, se houver. */
|
|
12
|
+
export function currentLabel() {
|
|
13
|
+
return cbStore.getStore()?.label || null;
|
|
14
|
+
}
|
package/src/test.js
CHANGED
|
@@ -153,5 +153,25 @@ assert.strictEqual(ev.billingUnit, "s3-put");
|
|
|
153
153
|
assert.ok(ev.costUSD > 0, "deve ter custo estimado");
|
|
154
154
|
console.log("✅ wrapAWS: evento capturado, cbLabel não vazou, resultado preservado");
|
|
155
155
|
|
|
156
|
+
// 3) expressMiddleware — label do contexto (ALS) entra sem cbLabel explícito
|
|
157
|
+
const mw = CloudBrokerCloud.expressMiddleware({
|
|
158
|
+
label: (req) => `modulo:${req.path}`,
|
|
159
|
+
product: (req) => (req.path.startsWith("/api/billing") ? "Billing" : "Outros"),
|
|
160
|
+
team: "Plataforma",
|
|
161
|
+
});
|
|
162
|
+
const fakeReq = { method: "GET", path: "/api/billing" };
|
|
163
|
+
await new Promise((resolve) => {
|
|
164
|
+
mw(fakeReq, {}, async () => {
|
|
165
|
+
await s3.send(new PutObjectCommand({ Bucket: "b", Key: "k", Body: "x" })); // sem cb* — herda do contexto
|
|
166
|
+
resolve();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
170
|
+
const ctxEvent = captured[captured.length - 1].events[0];
|
|
171
|
+
assert.strictEqual(ctxEvent.label, "modulo:/api/billing", `label do contexto esperado, veio ${ctxEvent.label}`);
|
|
172
|
+
assert.strictEqual(ctxEvent.product, "Billing", `product do contexto esperado, veio ${ctxEvent.product}`);
|
|
173
|
+
assert.strictEqual(ctxEvent.team, "Plataforma", `team do contexto esperado, veio ${ctxEvent.team}`);
|
|
174
|
+
console.log("✅ expressMiddleware: label/product/team do contexto aplicados sem cb* explícito");
|
|
175
|
+
|
|
156
176
|
globalThis.fetch = realFetch;
|
|
157
177
|
console.log("\n🎉 Todos os testes passaram.");
|