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 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: 5_000 }, // execução sob demanda
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` | `5000` | intervalo entre polls sem trabalho (mínimo `1000`) |
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.2.0" : "0.0.0-dev";
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
- process.on(
625
- "uncaughtException",
626
- markInternal((error) => {
627
- client2.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } }, getScope());
628
- void client2.flush(2e3).then(async () => {
629
- if (externalListenerCount("uncaughtException") === 0) {
630
- await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
631
- console.error(error);
632
- process.exit(1);
633
- }
634
- });
635
- })
636
- );
637
- process.on(
638
- "unhandledRejection",
639
- markInternal((reason) => {
640
- client2.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } }, getScope());
641
- void client2.flush(2e3);
642
- })
643
- );
644
- process.on(
645
- "beforeExit",
646
- markInternal(() => {
647
- void client2.flush(2e3);
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
- window.addEventListener("error", (event) => {
662
+ const onError = (event) => {
654
663
  client2.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } }, getScope());
655
- });
656
- window.addEventListener("unhandledrejection", (event) => {
664
+ };
665
+ const onRejection = (event) => {
657
666
  client2.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } }, getScope());
658
- });
659
- window.addEventListener("pagehide", () => {
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
  }