cloudbroker-sdk 0.5.0 → 0.7.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/.github/workflows/publish.yml +18 -0
- package/README.md +139 -5
- package/docs/COST_PER_FEATURE.md +296 -0
- package/example-checkout-app/.env.example +13 -0
- package/example-checkout-app/README.md +225 -0
- package/example-checkout-app/package.json +28 -0
- package/example-checkout-app/server.js +209 -0
- package/example-checkout-app/test/e2e.js +97 -0
- package/package.json +14 -4
- package/src/client.js +17 -6
- package/src/cloud-wrapper.js +531 -5
- package/src/pricing-cloud.js +165 -10
- package/src/pricing.js +161 -3
- package/src/test-azure-real.js +169 -0
- package/src/test-gemini-real.js +270 -0
- package/src/test.js +264 -0
- package/src/wrapper.js +125 -2
|
@@ -4,7 +4,16 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches: [main]
|
|
6
6
|
paths:
|
|
7
|
+
# Sem barra inicial: filtros de path do GitHub Actions são relativos à
|
|
8
|
+
# raiz do repo e '/package.json' não casa com nada — o workflow deixou de
|
|
9
|
+
# disparar por completo (o 0.6.0 foi mergeado e nunca publicado).
|
|
10
|
+
# 'package.json' já cobre só a raiz: sem '**', o glob não atravessa
|
|
11
|
+
# diretórios, que era a intenção original de ancorar.
|
|
7
12
|
- 'package.json'
|
|
13
|
+
# Escape hatch: quando o filtro acima não dispara (foi o caso do 0.6.0, que
|
|
14
|
+
# entrou no main sem publicar), dá pra publicar a versão que está no main sem
|
|
15
|
+
# precisar de um bump artificial só pra acionar o gatilho.
|
|
16
|
+
workflow_dispatch:
|
|
8
17
|
|
|
9
18
|
jobs:
|
|
10
19
|
publish:
|
|
@@ -22,6 +31,15 @@ jobs:
|
|
|
22
31
|
node-version: 20
|
|
23
32
|
registry-url: 'https://registry.npmjs.org'
|
|
24
33
|
|
|
34
|
+
# Gate de teste antes do publish — este pacote roda DENTRO da aplicação
|
|
35
|
+
# de produção do cliente; publicar sem rodar a suíte já era um risco
|
|
36
|
+
# aberto (nenhum CI de teste existia neste repo).
|
|
37
|
+
- name: Instalar dependências de teste
|
|
38
|
+
run: npm ci || npm install
|
|
39
|
+
|
|
40
|
+
- name: Rodar testes
|
|
41
|
+
run: npm test
|
|
42
|
+
|
|
25
43
|
- name: Publish to npm
|
|
26
44
|
run: npm publish
|
|
27
45
|
env:
|
package/README.md
CHANGED
|
@@ -46,6 +46,62 @@ const res = await anthropic.messages.create({
|
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Uso com Gemini
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { GoogleGenAI } from "@google/genai";
|
|
53
|
+
import { CloudBrokerAI } from "cloudbroker-sdk";
|
|
54
|
+
|
|
55
|
+
const gemini = CloudBrokerAI.wrap(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }), {
|
|
56
|
+
apiKey: "cb-key-xxxx",
|
|
57
|
+
product: "checkout",
|
|
58
|
+
team: "mobile",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const res = await gemini.models.generateContent({
|
|
62
|
+
model: "gemini-2.5-flash",
|
|
63
|
+
contents: "Olá!",
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
O `cbLabel` pode ir no próprio params (a API do Gemini é de argumento único) ou
|
|
68
|
+
no segundo argumento, como nos outros wrappers. Nos dois casos ele é removido
|
|
69
|
+
antes de a chamada seguir pro Google:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
await gemini.models.generateContent({
|
|
73
|
+
model: "gemini-2.5-flash", contents: "…", cbLabel: "checkout:resumo-pedido",
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Por que o custo do Gemini não é `input × preço + output × preço`
|
|
78
|
+
|
|
79
|
+
A conta do Gemini tem três particularidades. Ignorar qualquer uma delas erra o
|
|
80
|
+
valor de forma relevante, então o SDK trata as três:
|
|
81
|
+
|
|
82
|
+
| Particularidade | O que acontece se ignorar |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `promptTokenCount` **já inclui** os tokens servidos de cache, que custam ~10x menos | superfatura a parcela cacheada (1,65x a mais num prompt 80% cacheado) |
|
|
85
|
+
| `thoughtsTokenCount` é separado de `candidatesTokenCount` e é cobrado como output | subfatura — 33x a menos num caso com 2000 thinking tokens |
|
|
86
|
+
| 2.5 Pro dobra de preço acima de 200k tokens de prompt; áudio na entrada custa mais que texto | subfatura prompts longos e entrada de áudio |
|
|
87
|
+
|
|
88
|
+
### Limitações declaradas
|
|
89
|
+
|
|
90
|
+
- **O custo é estimado por tabela de preço local**, não lido de uma fatura. O
|
|
91
|
+
Gemini não expõe API de uso ou custo por chave — só o dashboard do AI Studio
|
|
92
|
+
e o console do Cloud Billing. Os preços são os do paid tier publicados em
|
|
93
|
+
`ai.google.dev/gemini-api/docs/pricing`, conferidos em 2026-08-02, e **não
|
|
94
|
+
foram calibrados contra fatura real**.
|
|
95
|
+
- **Free tier sai com custo > 0.** A tabela é a do paid tier; quem está no free
|
|
96
|
+
tier vê o valor que *pagaria*, não o que paga.
|
|
97
|
+
- **Modelo fora da tabela não recebe preço de outro modelo.** O evento vai com
|
|
98
|
+
`costUSD: null` e `unpriced: true`, preservando os tokens. Variante com sufixo
|
|
99
|
+
(`-preview-…`) é precificada pelo modelo base e marcada `approxPricing`.
|
|
100
|
+
- **Armazenamento de contexto em cache não é cobrado aqui.** O Google cobra por
|
|
101
|
+
hora de cache ativo (US$ 1,00/h na maioria dos modelos); isso não aparece no
|
|
102
|
+
`usageMetadata` de uma chamada e não é modelado.
|
|
103
|
+
- **Batch API não é modelada** — o Google cobra metade nesse modo.
|
|
104
|
+
|
|
49
105
|
## Streaming
|
|
50
106
|
|
|
51
107
|
Chamadas com `stream: true` também são rastreadas — o SDK observa os chunks
|
|
@@ -106,14 +162,17 @@ CloudBrokerAI.trackDeploy("cb-key-xxxx", {
|
|
|
106
162
|
});
|
|
107
163
|
```
|
|
108
164
|
|
|
109
|
-
##
|
|
165
|
+
## Cost per Feature — Rastreie Custo por Feature
|
|
110
166
|
|
|
111
|
-
Atribua custo de **AWS**
|
|
112
|
-
do
|
|
167
|
+
Atribua custo de **AWS** e **Azure** a cada feature/módulo sua. O SDK envolve um
|
|
168
|
+
client do provedor e, a cada chamada, estima o custo (requests + transferência) e
|
|
113
169
|
registra um evento — mesmo padrão fire-and-forget do wrapper de IA.
|
|
114
170
|
|
|
115
|
-
|
|
116
|
-
|
|
171
|
+
**👉 [Guia Completo](./docs/COST_PER_FEATURE.md)** — Setup (5 min), exemplos, FAQ.
|
|
172
|
+
|
|
173
|
+
> Escopo atual — AWS: **S3** e **DynamoDB** (`us-east-2`); Azure: **Blob Storage**
|
|
174
|
+
> e **Cosmos DB** (`eastus2`). Armazenamento GB-mês e atribuição automática
|
|
175
|
+
> (stack-trace) estão fora do MVP.
|
|
117
176
|
|
|
118
177
|
```js
|
|
119
178
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
@@ -146,6 +205,81 @@ Cada chamada gera um evento `POST /api/cloud/events` com `service`, `operation`,
|
|
|
146
205
|
`product`, `team`, `label`, `requests`, `bytesIn`/`bytesOut`, `costUSD` estimado e
|
|
147
206
|
`latencyMs`. O `cbLabel` nunca é repassado ao AWS SDK.
|
|
148
207
|
|
|
208
|
+
### Azure — Blob Storage e Cosmos DB
|
|
209
|
+
|
|
210
|
+
O Azure SDK não tem um `.send()` único como o AWS SDK v3: cada sub-client tem
|
|
211
|
+
métodos próprios. Por isso o `wrapAzure` intercepta a **cadeia de sub-clients** —
|
|
212
|
+
as fábricas (`getContainerClient`, `database`/`container`) devolvem sub-clients já
|
|
213
|
+
embrulhados, e só os métodos de data-plane conhecidos são rastreados. Qualquer
|
|
214
|
+
método fora dessa lista passa intocado.
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
import { BlobServiceClient } from "@azure/storage-blob";
|
|
218
|
+
import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
219
|
+
|
|
220
|
+
const blobSvc = CloudBrokerCloud.wrapAzure(
|
|
221
|
+
BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION),
|
|
222
|
+
{ apiKey: "cb-key-xxxx", product: "checkout", team: "mobile" },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// label opcional dentro do objeto de options nativo do método
|
|
226
|
+
await blobSvc
|
|
227
|
+
.getContainerClient("docs")
|
|
228
|
+
.getBlockBlobClient("nf.pdf")
|
|
229
|
+
.uploadData(buf, { cbLabel: "upload-nota-fiscal" });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Cosmos DB usa o **`requestCharge` (RUs) real** que o servidor devolve em cada
|
|
233
|
+
resposta — custo por unidade cobrada de verdade, não aproximação por request:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
import { CosmosClient } from "@azure/cosmos";
|
|
237
|
+
|
|
238
|
+
const cosmos = CloudBrokerCloud.wrapAzure(new CosmosClient(conn), {
|
|
239
|
+
apiKey: "cb-key-xxxx", product: "checkout",
|
|
240
|
+
});
|
|
241
|
+
const orders = cosmos.database("loja").container("pedidos");
|
|
242
|
+
|
|
243
|
+
await orders.items.create({ id: "123" }, { cbLabel: "criar-pedido" });
|
|
244
|
+
await orders.items.query("SELECT * FROM c").fetchAll(); // RUs do query também contam
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
O `expressMiddleware` (abaixo) funciona igual pros dois provedores — mesma
|
|
248
|
+
prioridade de `label`/`product`/`team`, sem precisar de `cbLabel` manual.
|
|
249
|
+
|
|
250
|
+
> **Precisão e premissas — leia antes de tratar o número como fechado:**
|
|
251
|
+
>
|
|
252
|
+
> - **Cosmos:** as RUs são as **reais** devolvidas pelo servidor em cada resposta
|
|
253
|
+
> (`requestCharge`). O **preço por RU** assume o modelo **serverless**. Em
|
|
254
|
+
> *throughput provisionado* (manual ou autoscale) a Azure cobra RU/s reservada
|
|
255
|
+
> por hora, independentemente do consumo — esse modelo ainda não é representado,
|
|
256
|
+
> então o custo calculado não corresponde à fatura nesse caso.
|
|
257
|
+
> - **Blob:** estimado por classe de operação (write/read/list), assumindo tier
|
|
258
|
+
> **Hot** e redundância **LRS**. Cool/Archive e GRS/ZRS têm outro preço.
|
|
259
|
+
> - **Transferência de saída:** cobrada sobre todo `bytesOut`. A Azure não cobra
|
|
260
|
+
> saída intra-região e concede franquia mensal — para um app rodando na mesma
|
|
261
|
+
> região do Storage, este termo **superestima**.
|
|
262
|
+
> - **Região:** só `eastus2` tem tabela de preço. Em outra região o evento é
|
|
263
|
+
> registrado com `costUSD: null` e `billingUnit: "*-region-unpriced"` — a
|
|
264
|
+
> operação aparece no relatório, mas sem custo inventado.
|
|
265
|
+
> - **Reconciliação:** a tela compara o estimado contra o custo real do Cost
|
|
266
|
+
> Management e mostra o **desvio**; o ajuste da tabela é manual (não há
|
|
267
|
+
> recalibração automática).
|
|
268
|
+
> - **Listagem:** `listBlobsFlat`/`listContainers` devolvem iterator preguiçoso —
|
|
269
|
+
> o evento só é emitido quando uma página é de fato buscada. Percorrendo com
|
|
270
|
+
> `for await`, contamos **1 operação** mesmo que o SDK pagine internamente;
|
|
271
|
+
> use `.byPage()` para contagem por página.
|
|
272
|
+
> - **Cosmos `for await` direto** (`getAsyncIterator`) não é instrumentado —
|
|
273
|
+
> use `fetchAll()`/`fetchNext()`.
|
|
274
|
+
> - **Delete** no tier Hot é grátis: sai com custo `0` e
|
|
275
|
+
> `billingUnit: "blob-free"`, sem sumir do relatório.
|
|
276
|
+
>
|
|
277
|
+
> ⚠️ **Ainda não calibrado contra uma fatura Azure real.** A estrutura é
|
|
278
|
+
> exercitada contra os SDKs oficiais (`@azure/storage-blob`, `@azure/cosmos`) na
|
|
279
|
+
> suíte de testes, mas os valores da tabela de preço Azure não foram conferidos
|
|
280
|
+
> contra a página oficial nem contra uma fatura. Trate o custo Azure como ordem
|
|
281
|
+
> de grandeza até rodar a reconciliação na sua conta.
|
|
282
|
+
|
|
149
283
|
### Atribuição automática por requisição (Express)
|
|
150
284
|
|
|
151
285
|
Em vez de passar `cbLabel` em cada `.send()`, use o middleware: todas as chamadas
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# CloudBroker SDK — Rastreie Custo por Feature
|
|
2
|
+
|
|
3
|
+
## O Que É?
|
|
4
|
+
|
|
5
|
+
Instrumentar sua aplicação com o CloudBroker SDK permite rastrear **quanto cada feature/módulo seu custa em AWS e Azure**.
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
// Seu código
|
|
9
|
+
await s3.putObject({ ... }, { cbLabel: 'invoice-generation' });
|
|
10
|
+
// CloudBroker registra: feature 'invoice-generation' custou $0.00042
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Result:** Dashboard mostra "invoice-generation: $X/mês".
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Setup Rápido (5 minutos)
|
|
18
|
+
|
|
19
|
+
### 1. Instalar
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install cloudbroker-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Gerar API Key
|
|
26
|
+
|
|
27
|
+
1. Acesse https://app.cloudbroker.app.br
|
|
28
|
+
2. Configurações → API Keys
|
|
29
|
+
3. Criar nova key do tipo **SDK** (só Admin pode criar keys SDK)
|
|
30
|
+
4. Copiar: `cb_live_...` — **o valor é exibido uma única vez**; depois só o
|
|
31
|
+
prefixo mascarado fica visível. Se perder, gere outra.
|
|
32
|
+
|
|
33
|
+
### 3. Instrumentar Backend
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
37
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
38
|
+
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
|
|
39
|
+
import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
40
|
+
|
|
41
|
+
// S3
|
|
42
|
+
const s3 = CloudBrokerCloud.wrapAWS(new S3Client({ region: "us-east-2" }), {
|
|
43
|
+
apiKey: process.env.CLOUDBROKER_API_KEY,
|
|
44
|
+
product: "checkout-service", // seu app
|
|
45
|
+
team: "payments", // seu squad
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// DynamoDB
|
|
49
|
+
const dynamodb = CloudBrokerCloud.wrapAWS(
|
|
50
|
+
DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-2" })),
|
|
51
|
+
{ apiKey: process.env.CLOUDBROKER_API_KEY, product: "checkout-service", team: "payments" }
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 4. Adicionar Labels
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// S3 upload com label
|
|
59
|
+
await s3.send(new PutObjectCommand({
|
|
60
|
+
Bucket: 'invoices',
|
|
61
|
+
Key: `${orderId}.pdf`,
|
|
62
|
+
Body: pdfBuffer,
|
|
63
|
+
}), {
|
|
64
|
+
cbLabel: 'invoice-generation', // ← Identifica a feature
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// DynamoDB insert com label
|
|
68
|
+
await dynamodb.send(new PutCommand({
|
|
69
|
+
TableName: 'Orders',
|
|
70
|
+
Item: { orderId, status: 'pending' },
|
|
71
|
+
}), {
|
|
72
|
+
cbLabel: 'order-processing', // ← Identifica a feature
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 5. Ver Custos
|
|
77
|
+
|
|
78
|
+
Dashboard: **CloudBroker App → Custo por Código**
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Feature: order-processing
|
|
82
|
+
Requests: 3,456
|
|
83
|
+
Total: $87.50
|
|
84
|
+
Per Request: $0.0253 ✅
|
|
85
|
+
|
|
86
|
+
Feature: invoice-generation
|
|
87
|
+
Requests: 1,234
|
|
88
|
+
Total: $10.20
|
|
89
|
+
Per Request: $0.00828 ✅
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Azure — Blob Storage e Cosmos DB
|
|
95
|
+
|
|
96
|
+
O SDK da Azure não tem um `.send()` único como o AWS SDK v3, então o `wrapAzure`
|
|
97
|
+
intercepta a **cadeia de sub-clients**: as fábricas (`getContainerClient`,
|
|
98
|
+
`database`/`container`) devolvem sub-clients já instrumentados.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
import { BlobServiceClient } from "@azure/storage-blob";
|
|
102
|
+
import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
103
|
+
|
|
104
|
+
const blobSvc = CloudBrokerCloud.wrapAzure(
|
|
105
|
+
BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION),
|
|
106
|
+
{ apiKey: "cb_live_...", product: "checkout", team: "mobile" },
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await blobSvc
|
|
110
|
+
.getContainerClient("docs")
|
|
111
|
+
.getBlockBlobClient("nf.pdf")
|
|
112
|
+
.uploadData(buf, { cbLabel: "upload-nota-fiscal" });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Cosmos DB usa o `requestCharge` (RUs) **real** devolvido pelo servidor:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
import { CosmosClient } from "@azure/cosmos";
|
|
119
|
+
|
|
120
|
+
const cosmos = CloudBrokerCloud.wrapAzure(new CosmosClient(conn), {
|
|
121
|
+
apiKey: "cb_live_...", product: "checkout",
|
|
122
|
+
});
|
|
123
|
+
const pedidos = cosmos.database("loja").container("pedidos");
|
|
124
|
+
|
|
125
|
+
await pedidos.items.create({ id: "123" }, { cbLabel: "criar-pedido" });
|
|
126
|
+
await pedidos.items.query("SELECT * FROM c").fetchAll();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
O `expressMiddleware` funciona igual para AWS e Azure.
|
|
130
|
+
|
|
131
|
+
**Premissas que mudam o número** (detalhe completo no
|
|
132
|
+
[README](../README.md#cost-per-feature--rastreie-custo-por-feature)):
|
|
133
|
+
|
|
134
|
+
| Item | Situação |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Cosmos — RUs | Reais (`requestCharge` do servidor) |
|
|
137
|
+
| Cosmos — preço por RU | Assume **serverless**. Throughput provisionado não é modelado |
|
|
138
|
+
| Blob | Estimado por classe de operação; assume tier **Hot** + **LRS** |
|
|
139
|
+
| Egress | Cobrado sobre todo `bytesOut` (a Azure não cobra intra-região) |
|
|
140
|
+
| Região | Só `eastus2` tem preço; outras saem com `costUSD: null` |
|
|
141
|
+
| `for await` no Cosmos | Não instrumentado — use `fetchAll()`/`fetchNext()` |
|
|
142
|
+
| Calibração | ⚠️ **Não conferido contra fatura Azure real ainda** |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Convenção de Labels
|
|
147
|
+
|
|
148
|
+
Use nomes **descritivos** que refletem features/módulos:
|
|
149
|
+
|
|
150
|
+
| Feature | Label | Exemplos de Operações |
|
|
151
|
+
|---------|-------|----------------------|
|
|
152
|
+
| Checkout | `order-processing` | Criar pedido, persistir dados |
|
|
153
|
+
| Geração de Invoice | `invoice-generation` | Upload de PDF, armazenamento |
|
|
154
|
+
| Detecção de Fraude | `fraud-detection` | Análise de padrões, lookup histórico |
|
|
155
|
+
| Analytics | `analytics-tracking` | Coleta de eventos, agregação |
|
|
156
|
+
| Admin Panel | `admin-reports` | Geração de relatórios, queries |
|
|
157
|
+
|
|
158
|
+
**Regras:**
|
|
159
|
+
- ✅ Use labels **finitos** (não criar um label por request)
|
|
160
|
+
- ✅ Labels **descritivos** (não `x1`, `x2`, etc)
|
|
161
|
+
- ✅ Use **kebab-case** (`order-processing`, não `orderProcessing`)
|
|
162
|
+
|
|
163
|
+
❌ **EVITAR:**
|
|
164
|
+
```javascript
|
|
165
|
+
// Ruim: label infinito (1 por request)
|
|
166
|
+
cbLabel: `order-${order.id}`
|
|
167
|
+
|
|
168
|
+
// Ruim: não descritivo
|
|
169
|
+
cbLabel: 'x1'
|
|
170
|
+
|
|
171
|
+
// Bom: finito e descritivo
|
|
172
|
+
cbLabel: 'order-processing'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Exemplo Completo
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
import express from 'express';
|
|
181
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
182
|
+
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
183
|
+
import { CloudBrokerCloud } from 'cloudbroker-sdk';
|
|
184
|
+
|
|
185
|
+
const app = express();
|
|
186
|
+
|
|
187
|
+
// Setup
|
|
188
|
+
const s3 = CloudBrokerCloud.wrapAWS(new S3Client(), {
|
|
189
|
+
apiKey: process.env.CLOUDBROKER_API_KEY,
|
|
190
|
+
product: 'checkout-service',
|
|
191
|
+
team: 'payments',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const dynamodb = CloudBrokerCloud.wrapAWS(
|
|
195
|
+
DynamoDBDocumentClient.from(new DynamoDBClient()),
|
|
196
|
+
{ apiKey: process.env.CLOUDBROKER_API_KEY, product: 'checkout-service', team: 'payments' }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// POST /orders — cria pedido com 2 features diferentes
|
|
200
|
+
app.post('/orders', async (req, res) => {
|
|
201
|
+
const order = { orderId: `order-${Date.now()}`, ...req.body };
|
|
202
|
+
|
|
203
|
+
// Feature 1: Persistir pedido
|
|
204
|
+
await dynamodb.send(new PutCommand({
|
|
205
|
+
TableName: 'Orders',
|
|
206
|
+
Item: order,
|
|
207
|
+
}), {
|
|
208
|
+
cbLabel: 'order-processing',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Feature 2: Gerar invoice
|
|
212
|
+
await s3.send(new PutObjectCommand({
|
|
213
|
+
Bucket: 'invoices',
|
|
214
|
+
Key: `${order.orderId}.json`,
|
|
215
|
+
Body: JSON.stringify(order),
|
|
216
|
+
}), {
|
|
217
|
+
cbLabel: 'invoice-generation',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
res.json({ ok: true, orderId: order.orderId });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
app.listen(3000);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Result no Dashboard:**
|
|
227
|
+
- `order-processing`: $X/mês
|
|
228
|
+
- `invoice-generation`: $Y/mês
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## FAQ
|
|
233
|
+
|
|
234
|
+
**P: Quanto tempo leva pra aparecer no dashboard?**
|
|
235
|
+
A: ~5 minutos (batch em DynamoDB).
|
|
236
|
+
|
|
237
|
+
**P: E se não colocar label?**
|
|
238
|
+
A: Ainda rastreia como `null` (agrupa sem label).
|
|
239
|
+
|
|
240
|
+
**P: Posso mudar o label depois?**
|
|
241
|
+
A: Não — é histórico. Use um novo label a partir de agora.
|
|
242
|
+
|
|
243
|
+
**P: Qual é o overhead?**
|
|
244
|
+
A: <1% CPU (fire-and-forget, não bloqueia requests).
|
|
245
|
+
|
|
246
|
+
**P: Funciona em todas as regiões?**
|
|
247
|
+
A: Hoje há tabela de preço para `us-east-2` (AWS) e `eastus2` (Azure). Em outra
|
|
248
|
+
região a operação é registrada com `costUSD: null` e `billingUnit`
|
|
249
|
+
`*-region-unpriced` — aparece no relatório, mas sem custo estimado (preferimos
|
|
250
|
+
não exibir o preço de outra região como se fosse o seu).
|
|
251
|
+
|
|
252
|
+
**P: E se der erro no rastreamento?**
|
|
253
|
+
A: Silencioso — seu request continua normal (fail-soft).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Troubleshooting
|
|
258
|
+
|
|
259
|
+
### Nenhum dado aparecendo no dashboard?
|
|
260
|
+
|
|
261
|
+
1. ✅ API Key configurada?
|
|
262
|
+
```bash
|
|
263
|
+
echo $CLOUDBROKER_API_KEY
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
2. ✅ CloudBrokerCloud.wrapAWS() chamado?
|
|
267
|
+
```javascript
|
|
268
|
+
const s3 = CloudBrokerCloud.wrapAWS(new S3Client(), { apiKey, product, team });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
3. ✅ cbLabel adicionado?
|
|
272
|
+
```javascript
|
|
273
|
+
await s3.send(cmd, { cbLabel: 'seu-label' });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
4. ✅ Esperou 5+ minutos?
|
|
277
|
+
(CloudBroker faz batch, não é real-time)
|
|
278
|
+
|
|
279
|
+
### Custo aparecendo como $0?
|
|
280
|
+
|
|
281
|
+
- Normal no MVP (estimativa simplificada, reconciliação depois)
|
|
282
|
+
- Operações muito baratas podem ficar abaixo do centavo
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Próximos Passos
|
|
287
|
+
|
|
288
|
+
- **Exemplo funcional:** Veja `example-checkout-app` no repo
|
|
289
|
+
- **Mais Serviços:** Lambda, SQS, SNS; Azure Functions, Service Bus (roadmap)
|
|
290
|
+
- **OpenTelemetry:** Rastrear custom operations (futuro)
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Suporte
|
|
295
|
+
|
|
296
|
+
Dúvidas? Abra issue no [GitHub](https://github.com/cloudbroker-app/cloudbroker-sdk).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# AWS Configuration
|
|
2
|
+
AWS_REGION=us-east-2
|
|
3
|
+
AWS_ACCESS_KEY_ID=your-access-key
|
|
4
|
+
AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
5
|
+
|
|
6
|
+
# CloudBroker Configuration
|
|
7
|
+
CLOUDBROKER_API_KEY=sk_1234...
|
|
8
|
+
CLOUDBROKER_PRODUCT=checkout-example
|
|
9
|
+
CLOUDBROKER_TEAM=platform
|
|
10
|
+
|
|
11
|
+
# App Configuration
|
|
12
|
+
PORT=3000
|
|
13
|
+
NODE_ENV=development
|