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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudbroker-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "CloudBroker SDK — Token Intelligence & Operational Financial Intelligence",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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.2.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.2.0",
96
+ sdkVersion: "0.4.0",
97
97
  sentAt: new Date().toISOString(),
98
98
  };
99
99
 
@@ -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
- const cbLabel = sendOptions?.cbLabel || null;
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 cbLabel pro AWS SDK.
60
- const { cbLabel: _omit, ...awsOptions } = sendOptions || {};
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: product || null,
74
- team: team || null,
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.");