hobots 2.2.0 → 2.3.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 +14 -2
- package/dist/index.cjs +196 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -3
- package/dist/index.d.ts +40 -3
- package/dist/index.js +196 -42
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +57 -1
- package/dist/protocol.d.ts +57 -1
- package/docs/api-reference.md +3 -0
- package/docs/protocol.md +22 -0
- package/docs/telemetry.md +11 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Hobots.init({
|
|
|
44
44
|
clientSecret: 'hb_<64 hex>',
|
|
45
45
|
instanceId: 'vm-cliente-01',
|
|
46
46
|
heartbeat: true, // telemetria: "estou vivo" a cada 30s
|
|
47
|
-
tasks: { pollIntervalMs:
|
|
47
|
+
tasks: { pollIntervalMs: 2_000 }, // execução sob demanda
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
Hobots.register('processar-nfe', async (params, ctx) => {
|
|
@@ -99,6 +99,7 @@ Hobots.init({
|
|
|
99
99
|
| `tracesSampleRate` | `number` | `1` | amostragem de transactions (0..1) |
|
|
100
100
|
| `maxBreadcrumbs` | `number` | `100` | tamanho do buffer de breadcrumbs |
|
|
101
101
|
| `captureUnhandled` | `boolean` | `true` | handlers globais de erro (`uncaughtException`/`unhandledRejection`) |
|
|
102
|
+
| `machineMetrics` | `boolean` | `true` | CPU/memória/disco no heartbeat, no poll e em `contexts.machine` dos eventos |
|
|
102
103
|
| `heartbeat` | `boolean \| HeartbeatOptions` | `false` | heartbeat periódico (`true` usa defaults) |
|
|
103
104
|
| `tasks` | `boolean \| TasksOptions` | `false` | habilita a execução sob demanda (`true` usa defaults) — veja [Tasks](#tasks-execução-sob-demanda) |
|
|
104
105
|
| `beforeSend` | `(event) => event \| null` | — | edita/descarta evento (retorne `null` para descartar) |
|
|
@@ -196,6 +197,17 @@ Hobots.init({
|
|
|
196
197
|
|
|
197
198
|
O heartbeat é **essencial** para agentes só de telemetria; com `tasks: true` ele é **opcional**, porque o poll de tasks já marca a presença do agente no painel. Conceito: [Heartbeat](./docs/concepts.md#heartbeat).
|
|
198
199
|
|
|
200
|
+
### Recursos da máquina (CPU, memória, disco)
|
|
201
|
+
|
|
202
|
+
Ligado por padrão em Node (`machineMetrics: true`), o SDK coleta CPU do sistema, memória, disco do volume do bot e RSS/CPU do próprio processo — **sem dependência nova** e sem timer próprio: a leitura pega carona em cada heartbeat e em cada poll de tasks (série da instância em **Instâncias → recursos da máquina**) e vai como `contexts.machine` em cada evento (card **"máquina no momento do erro"** no detalhe da issue). Assim dá para responder se o ambiente tem recurso suficiente e se um erro coincidiu com CPU/memória no limite.
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
Hobots.init({ clientSecret, instanceId: 'vm-01', heartbeat: true }); // coleta ligada
|
|
206
|
+
Hobots.init({ clientSecret, instanceId: 'vm-01', machineMetrics: false }); // desliga
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
O snapshot tem ~600 bytes (teto do servidor: 4 KiB). Formato em [Protocolo → MachineSnapshot](./docs/protocol.md#machinesnapshot).
|
|
210
|
+
|
|
199
211
|
### Flush e close
|
|
200
212
|
|
|
201
213
|
```js
|
|
@@ -241,7 +253,7 @@ process.on('SIGINT', () => {
|
|
|
241
253
|
|
|
242
254
|
| Opção (`TasksOptions`) | Tipo | Default | Descrição |
|
|
243
255
|
| --- | --- | --- | --- |
|
|
244
|
-
| `pollIntervalMs` | `number` | `
|
|
256
|
+
| `pollIntervalMs` | `number` | `2000` | intervalo entre polls sem trabalho (mínimo `1000`) |
|
|
245
257
|
| `concurrency` | `number` | `1` | runs simultâneas (clamp 1..5) |
|
|
246
258
|
| `taskTimeoutMs` | `number` | — | timeout client-side por run (não mata o handler; servidor é o backstop) |
|
|
247
259
|
| `logFlushIntervalMs` | `number` | `2000` | intervalo do envio incremental de logs |
|
package/dist/index.cjs
CHANGED
|
@@ -161,7 +161,7 @@ function scrubSecrets(text) {
|
|
|
161
161
|
|
|
162
162
|
// src/utils.ts
|
|
163
163
|
var SDK_NAME = "hobots";
|
|
164
|
-
var SDK_VERSION = true ? "2.
|
|
164
|
+
var SDK_VERSION = true ? "2.3.0" : "0.0.0-dev";
|
|
165
165
|
function isNode() {
|
|
166
166
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
167
167
|
}
|
|
@@ -454,6 +454,7 @@ var Client = class {
|
|
|
454
454
|
#options;
|
|
455
455
|
#config;
|
|
456
456
|
transport;
|
|
457
|
+
machine;
|
|
457
458
|
constructor(options, config = resolveConfig(options)) {
|
|
458
459
|
this.#options = options;
|
|
459
460
|
this.debug = !!options.debug;
|
|
@@ -473,6 +474,10 @@ var Client = class {
|
|
|
473
474
|
get config() {
|
|
474
475
|
return this.#config;
|
|
475
476
|
}
|
|
477
|
+
/** Sampler de recursos da máquina — cada evento passa a levar `contexts.machine`. */
|
|
478
|
+
attachMachineSampler(sampler) {
|
|
479
|
+
this.machine = sampler;
|
|
480
|
+
}
|
|
476
481
|
/** Serialização explícita nunca inclui o secret. */
|
|
477
482
|
toJSON() {
|
|
478
483
|
return { sdk: SDK_NAME, version: SDK_VERSION, baseUrl: this.#config.baseUrl };
|
|
@@ -504,6 +509,8 @@ var Client = class {
|
|
|
504
509
|
prepareEvent(event, hint, scope2) {
|
|
505
510
|
const tags = { ...this.options.tags, ...scope2.tags, ...event.tags, ...hint?.tags };
|
|
506
511
|
const extra = { ...scope2.extra, ...event.extra, ...hint?.extra };
|
|
512
|
+
const sampled = this.machine?.sample();
|
|
513
|
+
const machine = sampled ? { machine: sampled } : {};
|
|
507
514
|
return {
|
|
508
515
|
...event,
|
|
509
516
|
type: hint?.type ?? event.type ?? scope2.type ?? "error",
|
|
@@ -512,7 +519,7 @@ var Client = class {
|
|
|
512
519
|
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
513
520
|
extra: Object.keys(extra).length > 0 ? extra : void 0,
|
|
514
521
|
user: event.user ?? scope2.user,
|
|
515
|
-
contexts: { ...runtimeContexts(), ...scope2.contexts, ...event.contexts },
|
|
522
|
+
contexts: { ...runtimeContexts(), ...machine, ...scope2.contexts, ...event.contexts },
|
|
516
523
|
breadcrumbs: scope2.breadcrumbs.length > 0 ? [...scope2.breadcrumbs] : event.breadcrumbs,
|
|
517
524
|
fingerprint: hint?.fingerprint ?? event.fingerprint ?? scope2.fingerprint
|
|
518
525
|
};
|
|
@@ -547,8 +554,9 @@ var DEFAULT_INTERVAL_MS = 3e4;
|
|
|
547
554
|
var MIN_REPORTED_INTERVAL_MS = 1e3;
|
|
548
555
|
var MAX_REPORTED_INTERVAL_MS = 9e5;
|
|
549
556
|
var Heartbeat = class {
|
|
550
|
-
constructor(client2, options = {}, instanceId) {
|
|
557
|
+
constructor(client2, options = {}, instanceId, sampler) {
|
|
551
558
|
this.client = client2;
|
|
559
|
+
this.sampler = sampler;
|
|
552
560
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
553
561
|
this.instanceId = instanceId;
|
|
554
562
|
this.name = options.name ?? instanceId;
|
|
@@ -587,7 +595,9 @@ var Heartbeat = class {
|
|
|
587
595
|
name: this.name,
|
|
588
596
|
interval_ms: Math.min(Math.max(this.intervalMs, MIN_REPORTED_INTERVAL_MS), MAX_REPORTED_INTERVAL_MS),
|
|
589
597
|
timestamp: nowISO(),
|
|
590
|
-
metadata: this.metadata
|
|
598
|
+
metadata: this.metadata,
|
|
599
|
+
// recursos da máquina a cada beat — a série da instância no app
|
|
600
|
+
machine: this.sampler?.sample()
|
|
591
601
|
});
|
|
592
602
|
}
|
|
593
603
|
};
|
|
@@ -618,50 +628,184 @@ function hasInstalledGlobalHandlers() {
|
|
|
618
628
|
return installed;
|
|
619
629
|
}
|
|
620
630
|
function installGlobalHandlers(client2, getScope) {
|
|
621
|
-
if (installed) return;
|
|
631
|
+
if (installed) return () => void 0;
|
|
622
632
|
installed = true;
|
|
623
633
|
if (isNode()) {
|
|
624
|
-
|
|
625
|
-
"uncaughtException",
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
"
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return;
|
|
634
|
+
const onUncaught = markInternal((error) => {
|
|
635
|
+
client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
|
|
636
|
+
void client2.flush(2e3).then(async () => {
|
|
637
|
+
if (externalListenerCount("uncaughtException") === 0) {
|
|
638
|
+
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
639
|
+
console.error(error);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
const onRejection = markInternal((reason) => {
|
|
645
|
+
client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
|
|
646
|
+
void client2.flush(2e3);
|
|
647
|
+
});
|
|
648
|
+
const onBeforeExit = markInternal(() => {
|
|
649
|
+
void client2.flush(2e3);
|
|
650
|
+
});
|
|
651
|
+
process.on("uncaughtException", onUncaught);
|
|
652
|
+
process.on("unhandledRejection", onRejection);
|
|
653
|
+
process.on("beforeExit", onBeforeExit);
|
|
654
|
+
return () => {
|
|
655
|
+
process.off("uncaughtException", onUncaught);
|
|
656
|
+
process.off("unhandledRejection", onRejection);
|
|
657
|
+
process.off("beforeExit", onBeforeExit);
|
|
658
|
+
installed = false;
|
|
659
|
+
};
|
|
651
660
|
}
|
|
652
661
|
if (typeof window !== "undefined") {
|
|
653
|
-
|
|
662
|
+
const onError = (event) => {
|
|
654
663
|
client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
|
|
655
|
-
}
|
|
656
|
-
|
|
664
|
+
};
|
|
665
|
+
const onRejection = (event) => {
|
|
657
666
|
client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
|
|
658
|
-
}
|
|
659
|
-
|
|
667
|
+
};
|
|
668
|
+
const onPageHide = () => {
|
|
660
669
|
void client2.flush(1e3);
|
|
661
|
-
}
|
|
670
|
+
};
|
|
671
|
+
window.addEventListener("error", onError);
|
|
672
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
673
|
+
window.addEventListener("pagehide", onPageHide);
|
|
674
|
+
return () => {
|
|
675
|
+
window.removeEventListener("error", onError);
|
|
676
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
677
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
678
|
+
installed = false;
|
|
679
|
+
};
|
|
662
680
|
}
|
|
681
|
+
return () => {
|
|
682
|
+
installed = false;
|
|
683
|
+
};
|
|
663
684
|
}
|
|
664
685
|
|
|
686
|
+
// src/machine.ts
|
|
687
|
+
var MIN_CPU_WINDOW_MS = 1e3;
|
|
688
|
+
var round1 = (value) => Math.round(value * 10) / 10;
|
|
689
|
+
var clampPercent = (value) => Math.min(100, Math.max(0, value));
|
|
690
|
+
var MachineSampler = class {
|
|
691
|
+
constructor(instanceId, logger) {
|
|
692
|
+
this.instanceId = instanceId;
|
|
693
|
+
this.logger = logger;
|
|
694
|
+
}
|
|
695
|
+
os;
|
|
696
|
+
fs;
|
|
697
|
+
ready = false;
|
|
698
|
+
prevCpu;
|
|
699
|
+
prevProcessCpu;
|
|
700
|
+
prevAt = 0;
|
|
701
|
+
lastCpuPercent = null;
|
|
702
|
+
lastProcessCpuPercent = null;
|
|
703
|
+
/** Carrega os módulos e tira a leitura-base de CPU (a primeira amostra ainda sai com cpu_percent null). */
|
|
704
|
+
async start() {
|
|
705
|
+
if (!isNode()) return;
|
|
706
|
+
try {
|
|
707
|
+
this.os = await import("os");
|
|
708
|
+
this.fs = await import("fs");
|
|
709
|
+
this.prevCpu = this.cpuTotals();
|
|
710
|
+
this.prevProcessCpu = process.cpuUsage();
|
|
711
|
+
this.prevAt = Date.now();
|
|
712
|
+
this.ready = true;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
this.logger.debug("m\xE9tricas de m\xE1quina indispon\xEDveis neste runtime", error);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
sample() {
|
|
718
|
+
if (!this.ready || !this.os) return void 0;
|
|
719
|
+
try {
|
|
720
|
+
const os = this.os;
|
|
721
|
+
const cpuCount = Math.max(1, os.cpus().length);
|
|
722
|
+
this.updateCpu(cpuCount);
|
|
723
|
+
const snapshot = {
|
|
724
|
+
sampled_at: nowISO(),
|
|
725
|
+
instance_id: this.instanceId,
|
|
726
|
+
hostname: os.hostname(),
|
|
727
|
+
os: process.platform,
|
|
728
|
+
arch: process.arch,
|
|
729
|
+
cpu_count: cpuCount,
|
|
730
|
+
cpu_percent: this.lastCpuPercent,
|
|
731
|
+
process: {
|
|
732
|
+
pid: process.pid,
|
|
733
|
+
uptime_s: Math.round(process.uptime()),
|
|
734
|
+
cpu_percent: this.lastProcessCpuPercent
|
|
735
|
+
},
|
|
736
|
+
source: "stdlib"
|
|
737
|
+
};
|
|
738
|
+
if (process.platform !== "win32") {
|
|
739
|
+
const load = os.loadavg()[0];
|
|
740
|
+
if (typeof load === "number" && Number.isFinite(load)) snapshot.load_1 = round1(load);
|
|
741
|
+
}
|
|
742
|
+
const memTotal = os.totalmem();
|
|
743
|
+
const memFree = os.freemem();
|
|
744
|
+
if (memTotal > 0) {
|
|
745
|
+
snapshot.mem_total_bytes = memTotal;
|
|
746
|
+
snapshot.mem_available_bytes = memFree;
|
|
747
|
+
snapshot.mem_percent = round1(clampPercent((memTotal - memFree) / memTotal * 100));
|
|
748
|
+
}
|
|
749
|
+
const disk = this.diskUsage();
|
|
750
|
+
if (disk) Object.assign(snapshot, disk);
|
|
751
|
+
const mem = process.memoryUsage();
|
|
752
|
+
snapshot.process.rss_bytes = mem.rss;
|
|
753
|
+
snapshot.process.heap_used_bytes = mem.heapUsed;
|
|
754
|
+
return snapshot;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
this.logger.debug("falha ao coletar m\xE9tricas de m\xE1quina", error);
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
cpuTotals() {
|
|
761
|
+
let busy = 0;
|
|
762
|
+
let total = 0;
|
|
763
|
+
for (const cpu of this.os.cpus()) {
|
|
764
|
+
const t = cpu.times;
|
|
765
|
+
const cpuBusy = t.user + t.nice + t.sys + t.irq;
|
|
766
|
+
busy += cpuBusy;
|
|
767
|
+
total += cpuBusy + t.idle;
|
|
768
|
+
}
|
|
769
|
+
return { busy, total };
|
|
770
|
+
}
|
|
771
|
+
/** Média desde a leitura anterior; leituras com menos de 1s de janela reutilizam a última média. */
|
|
772
|
+
updateCpu(cpuCount) {
|
|
773
|
+
const now = Date.now();
|
|
774
|
+
const elapsedMs = now - this.prevAt;
|
|
775
|
+
if (elapsedMs < MIN_CPU_WINDOW_MS) return;
|
|
776
|
+
const cpu = this.cpuTotals();
|
|
777
|
+
if (this.prevCpu) {
|
|
778
|
+
const total = cpu.total - this.prevCpu.total;
|
|
779
|
+
if (total > 0) this.lastCpuPercent = round1(clampPercent((cpu.busy - this.prevCpu.busy) / total * 100));
|
|
780
|
+
}
|
|
781
|
+
this.prevCpu = cpu;
|
|
782
|
+
const usage = process.cpuUsage(this.prevProcessCpu);
|
|
783
|
+
const usedMicros = usage.user + usage.system;
|
|
784
|
+
this.lastProcessCpuPercent = round1(clampPercent(usedMicros / (elapsedMs * 1e3 * cpuCount) * 100));
|
|
785
|
+
this.prevProcessCpu = process.cpuUsage();
|
|
786
|
+
this.prevAt = now;
|
|
787
|
+
}
|
|
788
|
+
diskUsage() {
|
|
789
|
+
const statfs = this.fs?.statfsSync;
|
|
790
|
+
if (typeof statfs !== "function") return void 0;
|
|
791
|
+
try {
|
|
792
|
+
const path = process.cwd();
|
|
793
|
+
const stat = statfs(path);
|
|
794
|
+
const total = stat.bsize * stat.blocks;
|
|
795
|
+
const free = stat.bsize * stat.bavail;
|
|
796
|
+
if (total <= 0) return void 0;
|
|
797
|
+
return {
|
|
798
|
+
disk_path: path,
|
|
799
|
+
disk_total_bytes: total,
|
|
800
|
+
disk_free_bytes: free,
|
|
801
|
+
disk_percent: round1(clampPercent((total - free) / total * 100))
|
|
802
|
+
};
|
|
803
|
+
} catch {
|
|
804
|
+
return void 0;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
665
809
|
// src/scope.ts
|
|
666
810
|
var Scope = class _Scope {
|
|
667
811
|
tags = {};
|
|
@@ -1980,7 +2124,10 @@ var TaskAgent = class {
|
|
|
1980
2124
|
// o servidor calcula a tolerância de offline (3×) a partir disto em
|
|
1981
2125
|
// agentes poll-only — sem o campo, um poll lento flaparia offline/online.
|
|
1982
2126
|
// Clampado à faixa aceita pelo schema do poll (1s..15min).
|
|
1983
|
-
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5)
|
|
2127
|
+
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
|
|
2128
|
+
// recursos da máquina — alimenta a série da instância em agentes sem heartbeat
|
|
2129
|
+
// (o servidor grava no máx. 1 ponto/30s por instância)
|
|
2130
|
+
machine: this.options.machineSampler?.sample()
|
|
1984
2131
|
},
|
|
1985
2132
|
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
1986
2133
|
tasks: [...this.registry.keys()]
|
|
@@ -2128,7 +2275,9 @@ var DEFAULT_MAX_BREADCRUMBS = 100;
|
|
|
2128
2275
|
var client;
|
|
2129
2276
|
var scope = new Scope();
|
|
2130
2277
|
var heartbeat;
|
|
2278
|
+
var machineSampler;
|
|
2131
2279
|
var agent;
|
|
2280
|
+
var uninstallGlobalHandlers;
|
|
2132
2281
|
var registry = /* @__PURE__ */ new Map();
|
|
2133
2282
|
var warnedTasksDisabled = false;
|
|
2134
2283
|
function init(options) {
|
|
@@ -2141,21 +2290,26 @@ function init(options) {
|
|
|
2141
2290
|
void agent.stop(0);
|
|
2142
2291
|
agent = void 0;
|
|
2143
2292
|
}
|
|
2293
|
+
uninstallGlobalHandlers?.();
|
|
2294
|
+
uninstallGlobalHandlers = void 0;
|
|
2144
2295
|
warnedTasksDisabled = false;
|
|
2145
2296
|
const config = resolveConfig(options);
|
|
2146
2297
|
client = new Client(options, config);
|
|
2298
|
+
machineSampler = options.machineMetrics === false ? void 0 : new MachineSampler(options.instanceId, client.logger);
|
|
2299
|
+
void machineSampler?.start();
|
|
2300
|
+
client.attachMachineSampler(machineSampler);
|
|
2147
2301
|
if (options.captureUnhandled !== false) {
|
|
2148
|
-
installGlobalHandlers(client, () => scope);
|
|
2302
|
+
uninstallGlobalHandlers = installGlobalHandlers(client, () => scope);
|
|
2149
2303
|
}
|
|
2150
2304
|
if (options.heartbeat) {
|
|
2151
2305
|
const heartbeatOptions = options.heartbeat === true ? {} : options.heartbeat;
|
|
2152
|
-
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId);
|
|
2306
|
+
heartbeat = new Heartbeat(client, heartbeatOptions, options.instanceId, machineSampler);
|
|
2153
2307
|
heartbeat.start();
|
|
2154
2308
|
}
|
|
2155
2309
|
if (options.tasks) {
|
|
2156
2310
|
const tasksOptions = options.tasks === true ? {} : options.tasks;
|
|
2157
2311
|
agent = new TaskAgent(
|
|
2158
|
-
{ ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client },
|
|
2312
|
+
{ ...tasksOptions, config, instanceId: options.instanceId, debug: options.debug, client, machineSampler },
|
|
2159
2313
|
registry
|
|
2160
2314
|
);
|
|
2161
2315
|
}
|