@sonde/packs 0.1.1 → 0.1.3

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.
Files changed (161) hide show
  1. package/.turbo/turbo-build.log +4 -6
  2. package/.turbo/turbo-test.log +57 -814
  3. package/CHANGELOG.md +18 -0
  4. package/dist/docker/manifest (# Edit conflict 2026-02-19 LIl7ilN #).js +54 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +9 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/a10.d.ts +3 -0
  10. package/dist/integrations/a10.d.ts.map +1 -0
  11. package/dist/integrations/a10.js +218 -0
  12. package/dist/integrations/a10.js.map +1 -0
  13. package/dist/integrations/checkpoint.d.ts +3 -0
  14. package/dist/integrations/checkpoint.d.ts.map +1 -0
  15. package/dist/integrations/checkpoint.js +249 -0
  16. package/dist/integrations/checkpoint.js.map +1 -0
  17. package/dist/integrations/citrix.d.ts.map +1 -1
  18. package/dist/integrations/citrix.js +14 -19
  19. package/dist/integrations/citrix.js.map +1 -1
  20. package/dist/integrations/citrix.test.js +3 -3
  21. package/dist/integrations/citrix.test.js.map +1 -1
  22. package/dist/integrations/datadog.d.ts +3 -0
  23. package/dist/integrations/datadog.d.ts.map +1 -0
  24. package/dist/integrations/datadog.js +195 -0
  25. package/dist/integrations/datadog.js.map +1 -0
  26. package/dist/integrations/graph.d.ts.map +1 -1
  27. package/dist/integrations/graph.js +10 -15
  28. package/dist/integrations/graph.js.map +1 -1
  29. package/dist/integrations/graph.test.js +3 -3
  30. package/dist/integrations/graph.test.js.map +1 -1
  31. package/dist/integrations/httpbin.d.ts.map +1 -1
  32. package/dist/integrations/httpbin.js +2 -7
  33. package/dist/integrations/httpbin.js.map +1 -1
  34. package/dist/integrations/jira.d.ts +3 -0
  35. package/dist/integrations/jira.d.ts.map +1 -0
  36. package/dist/integrations/jira.js +199 -0
  37. package/dist/integrations/jira.js.map +1 -0
  38. package/dist/integrations/loki.d.ts +3 -0
  39. package/dist/integrations/loki.d.ts.map +1 -0
  40. package/dist/integrations/loki.js +178 -0
  41. package/dist/integrations/loki.js.map +1 -0
  42. package/dist/integrations/meraki.d.ts +3 -0
  43. package/dist/integrations/meraki.d.ts.map +1 -0
  44. package/dist/integrations/meraki.js +238 -0
  45. package/dist/integrations/meraki.js.map +1 -0
  46. package/dist/integrations/nutanix.d.ts.map +1 -1
  47. package/dist/integrations/nutanix.js +12 -17
  48. package/dist/integrations/nutanix.js.map +1 -1
  49. package/dist/integrations/nutanix.test.js +3 -3
  50. package/dist/integrations/nutanix.test.js.map +1 -1
  51. package/dist/integrations/pagerduty.d.ts +3 -0
  52. package/dist/integrations/pagerduty.d.ts.map +1 -0
  53. package/dist/integrations/pagerduty.js +229 -0
  54. package/dist/integrations/pagerduty.js.map +1 -0
  55. package/dist/integrations/proxmox.d.ts.map +1 -1
  56. package/dist/integrations/proxmox.js +8 -13
  57. package/dist/integrations/proxmox.js.map +1 -1
  58. package/dist/integrations/proxmox.test.js +3 -3
  59. package/dist/integrations/proxmox.test.js.map +1 -1
  60. package/dist/integrations/servicenow.d.ts.map +1 -1
  61. package/dist/integrations/servicenow.js +10 -16
  62. package/dist/integrations/servicenow.js.map +1 -1
  63. package/dist/integrations/servicenow.test.js +3 -3
  64. package/dist/integrations/servicenow.test.js.map +1 -1
  65. package/dist/integrations/splunk.d.ts.map +1 -1
  66. package/dist/integrations/splunk.js +8 -13
  67. package/dist/integrations/splunk.js.map +1 -1
  68. package/dist/integrations/splunk.test.js +3 -3
  69. package/dist/integrations/splunk.test.js.map +1 -1
  70. package/dist/integrations/thousandeyes.d.ts +3 -0
  71. package/dist/integrations/thousandeyes.d.ts.map +1 -0
  72. package/dist/integrations/thousandeyes.js +263 -0
  73. package/dist/integrations/thousandeyes.js.map +1 -0
  74. package/dist/integrations/vcenter.d.ts +3 -0
  75. package/dist/integrations/vcenter.d.ts.map +1 -0
  76. package/dist/integrations/vcenter.js +190 -0
  77. package/dist/integrations/vcenter.js.map +1 -0
  78. package/dist/proxmox/probes/ceph-status.test.d.ts (# Edit conflict 2026-02-19 N25hAvJ #).map +1 -0
  79. package/dist/system/index.d.ts.map +1 -1
  80. package/dist/system/index.js +10 -0
  81. package/dist/system/index.js.map +1 -1
  82. package/dist/system/manifest.d.ts.map +1 -1
  83. package/dist/system/manifest.js +100 -3
  84. package/dist/system/manifest.js.map +1 -1
  85. package/dist/system/probes/logs-dmesg.d.ts +13 -0
  86. package/dist/system/probes/logs-dmesg.d.ts.map +1 -0
  87. package/dist/system/probes/logs-dmesg.js +22 -0
  88. package/dist/system/probes/logs-dmesg.js.map +1 -0
  89. package/dist/system/probes/logs-dmesg.test.d.ts +2 -0
  90. package/dist/system/probes/logs-dmesg.test.d.ts.map +1 -0
  91. package/dist/system/probes/logs-dmesg.test.js +55 -0
  92. package/dist/system/probes/logs-dmesg.test.js.map +1 -0
  93. package/dist/system/probes/logs-journal.d.ts +21 -0
  94. package/dist/system/probes/logs-journal.d.ts.map +1 -0
  95. package/dist/system/probes/logs-journal.js +70 -0
  96. package/dist/system/probes/logs-journal.js.map +1 -0
  97. package/dist/system/probes/logs-journal.test.d.ts +2 -0
  98. package/dist/system/probes/logs-journal.test.d.ts.map +1 -0
  99. package/dist/system/probes/logs-journal.test.js +113 -0
  100. package/dist/system/probes/logs-journal.test.js.map +1 -0
  101. package/dist/system/probes/logs-tail.d.ts +14 -0
  102. package/dist/system/probes/logs-tail.d.ts.map +1 -0
  103. package/dist/system/probes/logs-tail.js +40 -0
  104. package/dist/system/probes/logs-tail.js.map +1 -0
  105. package/dist/system/probes/logs-tail.test.d.ts +2 -0
  106. package/dist/system/probes/logs-tail.test.d.ts.map +1 -0
  107. package/dist/system/probes/logs-tail.test.js +82 -0
  108. package/dist/system/probes/logs-tail.test.js.map +1 -0
  109. package/dist/system/probes/ping.d.ts +20 -0
  110. package/dist/system/probes/ping.d.ts.map +1 -0
  111. package/dist/system/probes/ping.js +54 -0
  112. package/dist/system/probes/ping.js.map +1 -0
  113. package/dist/system/probes/ping.test.d.ts +2 -0
  114. package/dist/system/probes/ping.test.d.ts.map +1 -0
  115. package/dist/system/probes/ping.test.js +127 -0
  116. package/dist/system/probes/ping.test.js.map +1 -0
  117. package/dist/system/probes/traceroute.d.ts +17 -0
  118. package/dist/system/probes/traceroute.d.ts.map +1 -0
  119. package/dist/system/probes/traceroute.js +72 -0
  120. package/dist/system/probes/traceroute.js.map +1 -0
  121. package/dist/system/probes/traceroute.test.d.ts +2 -0
  122. package/dist/system/probes/traceroute.test.d.ts.map +1 -0
  123. package/dist/system/probes/traceroute.test.js +98 -0
  124. package/dist/system/probes/traceroute.test.js.map +1 -0
  125. package/package.json +1 -1
  126. package/src/index.ts +9 -0
  127. package/src/integrations/a10.ts +370 -0
  128. package/src/integrations/checkpoint.ts +381 -0
  129. package/src/integrations/citrix.test.ts +3 -3
  130. package/src/integrations/citrix.ts +17 -21
  131. package/src/integrations/datadog.ts +281 -0
  132. package/src/integrations/graph.test.ts +3 -3
  133. package/src/integrations/graph.ts +10 -14
  134. package/src/integrations/httpbin.ts +2 -6
  135. package/src/integrations/jira.ts +272 -0
  136. package/src/integrations/loki.ts +228 -0
  137. package/src/integrations/meraki.ts +344 -0
  138. package/src/integrations/nutanix.test.ts +3 -3
  139. package/src/integrations/nutanix.ts +13 -17
  140. package/src/integrations/pagerduty.ts +319 -0
  141. package/src/integrations/proxmox.test.ts +3 -3
  142. package/src/integrations/proxmox.ts +8 -12
  143. package/src/integrations/servicenow.test.ts +3 -3
  144. package/src/integrations/servicenow.ts +10 -15
  145. package/src/integrations/splunk.test.ts +3 -3
  146. package/src/integrations/splunk.ts +9 -13
  147. package/src/integrations/thousandeyes.ts +353 -0
  148. package/src/integrations/vcenter.ts +261 -0
  149. package/src/system/index.ts +10 -0
  150. package/src/system/manifest.ts +113 -3
  151. package/src/system/probes/logs-dmesg.test.ts +83 -0
  152. package/src/system/probes/logs-dmesg.ts +38 -0
  153. package/src/system/probes/logs-journal.test.ts +142 -0
  154. package/src/system/probes/logs-journal.ts +103 -0
  155. package/src/system/probes/logs-tail.test.ts +140 -0
  156. package/src/system/probes/logs-tail.ts +70 -0
  157. package/src/system/probes/ping.test.ts +163 -0
  158. package/src/system/probes/ping.ts +89 -0
  159. package/src/system/probes/traceroute.test.ts +149 -0
  160. package/src/system/probes/traceroute.ts +99 -0
  161. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,261 @@
1
+ import type {
2
+ FetchFn,
3
+ IntegrationConfig,
4
+ IntegrationCredentials,
5
+ IntegrationPack,
6
+ IntegrationProbeHandler,
7
+ } from '@sonde/shared';
8
+
9
+ // --- Session-based auth ---
10
+
11
+ /** Session token cache: token + expiry timestamp */
12
+ let cachedSession: { token: string; expiresAt: number } | null = null;
13
+
14
+ /** Acquire a vCenter session token via POST /api/session */
15
+ async function getSessionToken(
16
+ config: IntegrationConfig,
17
+ credentials: IntegrationCredentials,
18
+ fetchFn: FetchFn,
19
+ ): Promise<string> {
20
+ const now = Date.now();
21
+ if (cachedSession && cachedSession.expiresAt > now) {
22
+ return cachedSession.token;
23
+ }
24
+
25
+ const { username, password } = credentials.credentials;
26
+ if (!username || !password) {
27
+ throw new Error('vCenter requires username and password credentials');
28
+ }
29
+
30
+ const encoded = Buffer.from(`${username}:${password}`).toString('base64');
31
+ const url = `${config.endpoint.replace(/\/$/, '')}/api/session`;
32
+ const res = await fetchFn(url, {
33
+ method: 'POST',
34
+ headers: {
35
+ Authorization: `Basic ${encoded}`,
36
+ Accept: 'application/json',
37
+ ...config.headers,
38
+ },
39
+ });
40
+
41
+ if (!res.ok) {
42
+ throw new Error(`vCenter session auth failed: ${res.status} ${res.statusText}`);
43
+ }
44
+
45
+ const token = ((await res.json()) as string).replace(/^"|"$/g, '');
46
+ cachedSession = { token, expiresAt: now + 5 * 60 * 1000 };
47
+ return token;
48
+ }
49
+
50
+ // --- REST helper ---
51
+
52
+ async function vcenterGet<T>(
53
+ path: string,
54
+ config: IntegrationConfig,
55
+ credentials: IntegrationCredentials,
56
+ fetchFn: FetchFn,
57
+ params?: Record<string, string>,
58
+ ): Promise<T> {
59
+ const token = await getSessionToken(config, credentials, fetchFn);
60
+ const base = `${config.endpoint.replace(/\/$/, '')}${path}`;
61
+ const url = new URL(base);
62
+ if (params) {
63
+ for (const [key, value] of Object.entries(params)) {
64
+ url.searchParams.set(key, value);
65
+ }
66
+ }
67
+
68
+ const headers: Record<string, string> = {
69
+ Accept: 'application/json',
70
+ 'vmware-api-session-id': token,
71
+ ...config.headers,
72
+ };
73
+
74
+ const res = await fetchFn(url.toString(), { headers });
75
+ if (!res.ok) {
76
+ throw new Error(`vCenter API returned ${res.status}: ${res.statusText}`);
77
+ }
78
+ return (await res.json()) as T;
79
+ }
80
+
81
+ // --- Probe handlers ---
82
+
83
+ const vmList: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
84
+ const vms = await vcenterGet<
85
+ Array<{
86
+ vm: string;
87
+ name: string;
88
+ power_state: string;
89
+ cpu_count?: number;
90
+ memory_size_MiB?: number;
91
+ }>
92
+ >('/api/vcenter/vm', config, credentials, fetchFn);
93
+
94
+ return {
95
+ vms: vms.map((vm) => ({
96
+ vm: vm.vm,
97
+ name: vm.name,
98
+ powerState: vm.power_state,
99
+ cpuCount: vm.cpu_count ?? null,
100
+ memorySizeMiB: vm.memory_size_MiB ?? null,
101
+ })),
102
+ count: vms.length,
103
+ };
104
+ };
105
+
106
+ const vmDetail: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
107
+ const vm = params?.vm as string;
108
+ if (!vm) throw new Error('vm parameter is required (VM identifier)');
109
+
110
+ return vcenterGet(`/api/vcenter/vm/${vm}`, config, credentials, fetchFn);
111
+ };
112
+
113
+ const hostList: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
114
+ const hosts = await vcenterGet<
115
+ Array<{
116
+ host: string;
117
+ name: string;
118
+ connection_state: string;
119
+ power_state?: string;
120
+ }>
121
+ >('/api/vcenter/host', config, credentials, fetchFn);
122
+
123
+ return {
124
+ hosts: hosts.map((h) => ({
125
+ host: h.host,
126
+ name: h.name,
127
+ connectionState: h.connection_state,
128
+ powerState: h.power_state ?? null,
129
+ })),
130
+ count: hosts.length,
131
+ };
132
+ };
133
+
134
+ const datastoreList: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
135
+ const datastores = await vcenterGet<
136
+ Array<{
137
+ datastore: string;
138
+ name: string;
139
+ type: string;
140
+ capacity?: number;
141
+ free_space?: number;
142
+ }>
143
+ >('/api/vcenter/datastore', config, credentials, fetchFn);
144
+
145
+ return {
146
+ datastores: datastores.map((ds) => ({
147
+ datastore: ds.datastore,
148
+ name: ds.name,
149
+ type: ds.type,
150
+ capacity: ds.capacity ?? 0,
151
+ freeSpace: ds.free_space ?? 0,
152
+ })),
153
+ count: datastores.length,
154
+ };
155
+ };
156
+
157
+ const clusterList: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
158
+ const clusters = await vcenterGet<
159
+ Array<{
160
+ cluster: string;
161
+ name: string;
162
+ ha_enabled?: boolean;
163
+ drs_enabled?: boolean;
164
+ }>
165
+ >('/api/vcenter/cluster', config, credentials, fetchFn);
166
+
167
+ return {
168
+ clusters: clusters.map((cl) => ({
169
+ cluster: cl.cluster,
170
+ name: cl.name,
171
+ haEnabled: cl.ha_enabled ?? false,
172
+ drsEnabled: cl.drs_enabled ?? false,
173
+ })),
174
+ count: clusters.length,
175
+ };
176
+ };
177
+
178
+ const health: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
179
+ return vcenterGet('/api/vcenter/system/health', config, credentials, fetchFn);
180
+ };
181
+
182
+ // --- Pack definition ---
183
+
184
+ export const vcenterPack: IntegrationPack = {
185
+ manifest: {
186
+ name: 'vcenter',
187
+ type: 'integration',
188
+ version: '0.1.0',
189
+ description: 'VMware vCenter — VMs, ESXi hosts, datastores, clusters, and health status',
190
+ requires: { groups: [], files: [], commands: [] },
191
+ probes: [
192
+ {
193
+ name: 'vm.list',
194
+ description: 'List all VMs with power state, CPU, and memory',
195
+ capability: 'observe',
196
+ params: {},
197
+ timeout: 15000,
198
+ },
199
+ {
200
+ name: 'vm.detail',
201
+ description: 'Detailed info for a single VM by identifier',
202
+ capability: 'observe',
203
+ params: {
204
+ vm: { type: 'string', description: 'VM identifier (e.g. vm-42)', required: true },
205
+ },
206
+ timeout: 15000,
207
+ },
208
+ {
209
+ name: 'host.list',
210
+ description: 'List ESXi hosts with connection state',
211
+ capability: 'observe',
212
+ params: {},
213
+ timeout: 15000,
214
+ },
215
+ {
216
+ name: 'datastore.list',
217
+ description: 'List datastores with capacity and free space',
218
+ capability: 'observe',
219
+ params: {},
220
+ timeout: 15000,
221
+ },
222
+ {
223
+ name: 'cluster.list',
224
+ description: 'List clusters with HA and DRS status',
225
+ capability: 'observe',
226
+ params: {},
227
+ timeout: 15000,
228
+ },
229
+ {
230
+ name: 'health',
231
+ description: 'Overall vCenter health status',
232
+ capability: 'observe',
233
+ params: {},
234
+ timeout: 15000,
235
+ },
236
+ ],
237
+ runbook: {
238
+ category: 'vmware',
239
+ probes: ['health', 'host.list', 'vm.list'],
240
+ parallel: true,
241
+ },
242
+ },
243
+
244
+ handlers: {
245
+ 'vm.list': vmList,
246
+ 'vm.detail': vmDetail,
247
+ 'host.list': hostList,
248
+ 'datastore.list': datastoreList,
249
+ 'cluster.list': clusterList,
250
+ health,
251
+ },
252
+
253
+ testConnection: async (config, credentials, fetchFn) => {
254
+ try {
255
+ await getSessionToken(config, credentials, fetchFn);
256
+ return true;
257
+ } catch {
258
+ return false;
259
+ }
260
+ },
261
+ };
@@ -2,7 +2,12 @@ import type { Pack } from '../types.js';
2
2
  import { systemManifest } from './manifest.js';
3
3
  import { cpuUsage } from './probes/cpu-usage.js';
4
4
  import { diskUsage } from './probes/disk-usage.js';
5
+ import { logsDmesg } from './probes/logs-dmesg.js';
6
+ import { logsJournal } from './probes/logs-journal.js';
7
+ import { logsTail } from './probes/logs-tail.js';
5
8
  import { memoryUsage } from './probes/memory-usage.js';
9
+ import { ping } from './probes/ping.js';
10
+ import { traceroute } from './probes/traceroute.js';
6
11
 
7
12
  export const systemPack: Pack = {
8
13
  manifest: systemManifest,
@@ -10,5 +15,10 @@ export const systemPack: Pack = {
10
15
  'system.disk.usage': diskUsage,
11
16
  'system.memory.usage': memoryUsage,
12
17
  'system.cpu.usage': cpuUsage,
18
+ 'system.network.ping': ping,
19
+ 'system.logs.journal': logsJournal,
20
+ 'system.logs.dmesg': logsDmesg,
21
+ 'system.logs.tail': logsTail,
22
+ 'system.network.traceroute': traceroute,
13
23
  },
14
24
  };
@@ -2,12 +2,20 @@ import type { PackManifest } from '@sonde/shared';
2
2
 
3
3
  export const systemManifest: PackManifest = {
4
4
  name: 'system',
5
- version: '0.1.0',
6
- description: 'Basic system metrics: disk usage, memory usage, CPU load',
5
+ version: '0.2.0',
6
+ description:
7
+ 'System metrics, logs, and network diagnostics: disk, memory, CPU, journal, dmesg, log tail, ping, traceroute',
7
8
  requires: {
8
9
  groups: [],
9
10
  files: [],
10
- commands: ['df'],
11
+ commands: [
12
+ 'df',
13
+ 'ping',
14
+ 'journalctl',
15
+ 'dmesg',
16
+ 'tail',
17
+ 'traceroute',
18
+ ],
11
19
  },
12
20
  probes: [
13
21
  {
@@ -28,6 +36,108 @@ export const systemManifest: PackManifest = {
28
36
  capability: 'observe',
29
37
  timeout: 10_000,
30
38
  },
39
+ {
40
+ name: 'network.ping',
41
+ description:
42
+ 'Ping a remote host to test ICMP reachability and measure latency',
43
+ capability: 'observe',
44
+ timeout: 20_000,
45
+ params: {
46
+ host: {
47
+ type: 'string',
48
+ description: 'Hostname or IP address to ping',
49
+ required: true,
50
+ },
51
+ count: {
52
+ type: 'number',
53
+ description:
54
+ 'Number of ping packets to send (default 4, max 20)',
55
+ required: false,
56
+ },
57
+ },
58
+ },
59
+ {
60
+ name: 'logs.journal',
61
+ description:
62
+ 'Recent systemd journal entries (Linux only)',
63
+ capability: 'observe',
64
+ timeout: 15_000,
65
+ params: {
66
+ unit: {
67
+ type: 'string',
68
+ description:
69
+ 'Filter to a specific systemd unit (e.g. "nginx")',
70
+ required: false,
71
+ },
72
+ lines: {
73
+ type: 'number',
74
+ description:
75
+ 'Number of entries to return (default 50, max 500)',
76
+ required: false,
77
+ },
78
+ priority: {
79
+ type: 'string',
80
+ description:
81
+ 'Syslog priority filter (e.g. "err", "warning")',
82
+ required: false,
83
+ },
84
+ },
85
+ },
86
+ {
87
+ name: 'logs.dmesg',
88
+ description: 'Kernel ring buffer (dmesg) output',
89
+ capability: 'observe',
90
+ timeout: 10_000,
91
+ params: {
92
+ lines: {
93
+ type: 'number',
94
+ description:
95
+ 'Number of lines to return (default 50, max 500)',
96
+ required: false,
97
+ },
98
+ },
99
+ },
100
+ {
101
+ name: 'logs.tail',
102
+ description:
103
+ 'Tail a log file (restricted to /var/log/ and /tmp/)',
104
+ capability: 'observe',
105
+ timeout: 10_000,
106
+ params: {
107
+ path: {
108
+ type: 'string',
109
+ description:
110
+ 'Absolute path to log file (must be under /var/log/ or /tmp/)',
111
+ required: true,
112
+ },
113
+ lines: {
114
+ type: 'number',
115
+ description:
116
+ 'Number of lines to return (default 50, max 500)',
117
+ required: false,
118
+ },
119
+ },
120
+ },
121
+ {
122
+ name: 'network.traceroute',
123
+ description:
124
+ 'Trace network path to a host showing each hop and latency',
125
+ capability: 'observe',
126
+ timeout: 60_000,
127
+ params: {
128
+ host: {
129
+ type: 'string',
130
+ description: 'Hostname or IP address to trace',
131
+ required: true,
132
+ },
133
+ maxHops: {
134
+ type: 'number',
135
+ description:
136
+ 'Maximum number of hops (default 30, max 64)',
137
+ required: false,
138
+ },
139
+ },
140
+ },
31
141
  ],
32
142
  runbook: {
33
143
  category: 'system',
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { DmesgResult } from './logs-dmesg.js';
4
+ import { logsDmesg, parseDmesgOutput } from './logs-dmesg.js';
5
+
6
+ const DMESG_OUTPUT = `[2024-01-15T10:00:01+0000] EXT4-fs (sda1): mounted filesystem
7
+ [2024-01-15T10:00:02+0000] audit: type=1400 msg=avc:
8
+ [2024-01-15T10:00:03+0000] NET: Registered PF_INET6
9
+ [2024-01-15T10:00:04+0000] usb 1-1: new high-speed USB device
10
+ [2024-01-15T10:00:05+0000] e1000: Intel(R) PRO/1000 Network Driver`;
11
+
12
+ describe('parseDmesgOutput', () => {
13
+ it('returns last N lines', () => {
14
+ const result = parseDmesgOutput(DMESG_OUTPUT, 3);
15
+
16
+ expect(result.lineCount).toBe(3);
17
+ expect(result.lines).toHaveLength(3);
18
+ expect(result.lines[0]).toContain('NET: Registered PF_INET6');
19
+ expect(result.lines[2]).toContain('e1000');
20
+ });
21
+
22
+ it('returns all lines when lines exceeds total', () => {
23
+ const result = parseDmesgOutput(DMESG_OUTPUT, 100);
24
+
25
+ expect(result.lineCount).toBe(5);
26
+ expect(result.lines).toHaveLength(5);
27
+ });
28
+
29
+ it('handles empty output', () => {
30
+ const result = parseDmesgOutput('', 50);
31
+
32
+ expect(result.lineCount).toBe(0);
33
+ expect(result.lines).toHaveLength(0);
34
+ });
35
+
36
+ it('handles single line', () => {
37
+ const result = parseDmesgOutput(
38
+ '[0.000000] Linux version 6.1.0',
39
+ 50,
40
+ );
41
+
42
+ expect(result.lineCount).toBe(1);
43
+ expect(result.lines[0]).toContain('Linux version');
44
+ });
45
+ });
46
+
47
+ describe('logsDmesg handler', () => {
48
+ it('passes correct args on Linux', async () => {
49
+ const calls: Array<{ cmd: string; args: string[] }> = [];
50
+ const mockExec: ExecFn = async (cmd, args) => {
51
+ calls.push({ cmd, args });
52
+ return DMESG_OUTPUT;
53
+ };
54
+
55
+ const result = (await logsDmesg(
56
+ undefined,
57
+ mockExec,
58
+ )) as DmesgResult;
59
+
60
+ expect(calls).toHaveLength(1);
61
+ expect(calls[0]?.cmd).toBe('dmesg');
62
+ // On the CI/test runner platform, args will vary
63
+ // but the handler should call dmesg
64
+ expect(result.lineCount).toBeGreaterThan(0);
65
+ });
66
+
67
+ it('clamps lines to valid range', async () => {
68
+ const mockExec: ExecFn = async () => DMESG_OUTPUT;
69
+
70
+ const result = (await logsDmesg(
71
+ { lines: 2 },
72
+ mockExec,
73
+ )) as DmesgResult;
74
+ expect(result.lineCount).toBe(2);
75
+
76
+ const result2 = (await logsDmesg(
77
+ { lines: 9999 },
78
+ mockExec,
79
+ )) as DmesgResult;
80
+ // 500 clamped but only 5 lines in fixture
81
+ expect(result2.lineCount).toBe(5);
82
+ });
83
+ });
@@ -0,0 +1,38 @@
1
+ import { platform } from 'node:os';
2
+ import type { ProbeHandler } from '../../types.js';
3
+
4
+ export interface DmesgResult {
5
+ lines: string[];
6
+ lineCount: number;
7
+ }
8
+
9
+ /**
10
+ * Reads the kernel ring buffer via `dmesg`.
11
+ * On Linux uses `--time-format iso` for readable timestamps.
12
+ * On macOS uses plain `dmesg` (no --time-format support).
13
+ */
14
+ export const logsDmesg: ProbeHandler = async (params, exec) => {
15
+ const lines = Math.min(
16
+ Math.max(Number(params?.lines ?? 50), 1),
17
+ 500,
18
+ );
19
+
20
+ const isMac = platform() === 'darwin';
21
+ const dmesgArgs = isMac ? [] : ['--time-format', 'iso'];
22
+
23
+ const output = await exec('dmesg', dmesgArgs);
24
+ return parseDmesgOutput(output, lines);
25
+ };
26
+
27
+ export function parseDmesgOutput(
28
+ raw: string,
29
+ lines: number,
30
+ ): DmesgResult {
31
+ const allLines = raw.trim().split('\n').filter(Boolean);
32
+ const tailLines = allLines.slice(-lines);
33
+
34
+ return {
35
+ lines: tailLines,
36
+ lineCount: tailLines.length,
37
+ };
38
+ }
@@ -0,0 +1,142 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { JournalResult } from './logs-journal.js';
4
+ import { logsJournal, parseJournalOutput } from './logs-journal.js';
5
+
6
+ vi.mock('node:os', () => ({ platform: () => 'linux' }));
7
+
8
+ const JOURNAL_JSON_OUTPUT = [
9
+ JSON.stringify({
10
+ __REALTIME_TIMESTAMP: '1700000000000000',
11
+ PRIORITY: '6',
12
+ MESSAGE: 'Started Session 42 of User root.',
13
+ _PID: '1',
14
+ _UID: '0',
15
+ _SYSTEMD_UNIT: 'session-42.scope',
16
+ }),
17
+ JSON.stringify({
18
+ __REALTIME_TIMESTAMP: '1700000001000000',
19
+ PRIORITY: '3',
20
+ MESSAGE: 'Failed to start nginx.service',
21
+ _PID: '512',
22
+ _UID: '0',
23
+ _SYSTEMD_UNIT: 'nginx.service',
24
+ }),
25
+ ].join('\n');
26
+
27
+ const SINGLE_ENTRY = JSON.stringify({
28
+ __REALTIME_TIMESTAMP: '1700000000000000',
29
+ PRIORITY: '4',
30
+ MESSAGE: 'Warning from sshd',
31
+ _PID: '100',
32
+ _UID: '0',
33
+ });
34
+
35
+ describe('parseJournalOutput', () => {
36
+ it('parses multiple JSON journal entries', () => {
37
+ const result = parseJournalOutput(JOURNAL_JSON_OUTPUT);
38
+
39
+ expect(result.entries).toHaveLength(2);
40
+ expect(result.entryCount).toBe(2);
41
+ expect(result.unit).toBeUndefined();
42
+
43
+ expect(result.entries[0]).toEqual({
44
+ timestamp: '2023-11-14T22:13:20.000Z',
45
+ priority: 6,
46
+ message: 'Started Session 42 of User root.',
47
+ pid: 1,
48
+ uid: 0,
49
+ unit: 'session-42.scope',
50
+ });
51
+
52
+ expect(result.entries[1]).toEqual({
53
+ timestamp: '2023-11-14T22:13:21.000Z',
54
+ priority: 3,
55
+ message: 'Failed to start nginx.service',
56
+ pid: 512,
57
+ uid: 0,
58
+ unit: 'nginx.service',
59
+ });
60
+ });
61
+
62
+ it('sets unit in result when filtering by unit', () => {
63
+ const result = parseJournalOutput(SINGLE_ENTRY, 'sshd');
64
+
65
+ expect(result.unit).toBe('sshd');
66
+ expect(result.entries).toHaveLength(1);
67
+ });
68
+
69
+ it('skips malformed JSON lines', () => {
70
+ const input = `not json at all\n${SINGLE_ENTRY}\n{broken`;
71
+ const result = parseJournalOutput(input);
72
+
73
+ expect(result.entries).toHaveLength(1);
74
+ expect(result.entries[0]?.message).toBe('Warning from sshd');
75
+ });
76
+
77
+ it('handles empty output', () => {
78
+ const result = parseJournalOutput('');
79
+ expect(result.entries).toHaveLength(0);
80
+ expect(result.entryCount).toBe(0);
81
+ });
82
+
83
+ it('handles entry without _SYSTEMD_UNIT', () => {
84
+ const result = parseJournalOutput(SINGLE_ENTRY);
85
+ expect(result.entries[0]?.unit).toBeUndefined();
86
+ });
87
+ });
88
+
89
+ describe('logsJournal handler', () => {
90
+ it('passes correct args to exec with defaults', async () => {
91
+ const calls: Array<{ cmd: string; args: string[] }> = [];
92
+ const mockExec: ExecFn = async (cmd, args) => {
93
+ calls.push({ cmd, args });
94
+ return SINGLE_ENTRY;
95
+ };
96
+
97
+ const result = (await logsJournal(
98
+ undefined,
99
+ mockExec,
100
+ )) as JournalResult;
101
+
102
+ expect(calls).toHaveLength(1);
103
+ expect(calls[0]?.cmd).toBe('journalctl');
104
+ expect(calls[0]?.args).toEqual([
105
+ '-n', '50', '--no-pager', '-o', 'json',
106
+ ]);
107
+ expect(result.entryCount).toBe(1);
108
+ });
109
+
110
+ it('passes unit and priority params', async () => {
111
+ const calls: Array<{ cmd: string; args: string[] }> = [];
112
+ const mockExec: ExecFn = async (cmd, args) => {
113
+ calls.push({ cmd, args });
114
+ return SINGLE_ENTRY;
115
+ };
116
+
117
+ await logsJournal(
118
+ { unit: 'nginx', priority: 'err', lines: 100 },
119
+ mockExec,
120
+ );
121
+
122
+ expect(calls[0]?.args).toEqual([
123
+ '-n', '100', '--no-pager', '-o', 'json',
124
+ '-u', 'nginx',
125
+ '-p', 'err',
126
+ ]);
127
+ });
128
+
129
+ it('clamps lines to valid range', async () => {
130
+ const calls: Array<{ cmd: string; args: string[] }> = [];
131
+ const mockExec: ExecFn = async (cmd, args) => {
132
+ calls.push({ cmd, args });
133
+ return SINGLE_ENTRY;
134
+ };
135
+
136
+ await logsJournal({ lines: 9999 }, mockExec);
137
+ expect(calls[0]?.args).toContain('500');
138
+
139
+ await logsJournal({ lines: 0 }, mockExec);
140
+ expect(calls[1]?.args).toContain('1');
141
+ });
142
+ });