@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
@@ -189,10 +189,10 @@ describe('graph pack', () => {
189
189
  expect(result).toBe(false);
190
190
  });
191
191
 
192
- it('returns false on network error', async () => {
192
+ it('throws on network error', async () => {
193
193
  const fetchFn = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
194
- const result = await graphPack.testConnection(graphConfig, graphCreds, fetchFn);
195
- expect(result).toBe(false);
194
+ await expect(graphPack.testConnection(graphConfig, graphCreds, fetchFn))
195
+ .rejects.toThrow('ECONNREFUSED');
196
196
  });
197
197
  });
198
198
 
@@ -395,19 +395,15 @@ export const graphPack: IntegrationPack = {
395
395
  },
396
396
 
397
397
  testConnection: async (config, credentials, fetchFn) => {
398
- try {
399
- const token = await ensureGraphToken(credentials, fetchFn);
400
- const url = `${config.endpoint.replace(/\/$/, '')}/organization?$select=id&$top=1`;
401
- const res = await fetchFn(url, {
402
- headers: {
403
- Authorization: `Bearer ${token}`,
404
- Accept: 'application/json',
405
- ...config.headers,
406
- },
407
- });
408
- return res.ok;
409
- } catch {
410
- return false;
411
- }
398
+ const token = await ensureGraphToken(credentials, fetchFn);
399
+ const url = `${config.endpoint.replace(/\/$/, '')}/organization?$select=id&$top=1`;
400
+ const res = await fetchFn(url, {
401
+ headers: {
402
+ Authorization: `Bearer ${token}`,
403
+ Accept: 'application/json',
404
+ ...config.headers,
405
+ },
406
+ });
407
+ return res.ok;
412
408
  },
413
409
  };
@@ -62,11 +62,7 @@ export const httpbinPack: IntegrationPack = {
62
62
  },
63
63
 
64
64
  testConnection: async (config, _credentials, fetchFn) => {
65
- try {
66
- const res = await fetchFn(`${config.endpoint}/ip`);
67
- return res.ok;
68
- } catch {
69
- return false;
70
- }
65
+ const res = await fetchFn(`${config.endpoint}/ip`);
66
+ return res.ok;
71
67
  },
72
68
  };
@@ -0,0 +1,272 @@
1
+ import type {
2
+ FetchFn,
3
+ IntegrationConfig,
4
+ IntegrationCredentials,
5
+ IntegrationPack,
6
+ IntegrationProbeHandler,
7
+ } from '@sonde/shared';
8
+
9
+ // --- Auth helpers ---
10
+
11
+ /** Build Basic auth header from email + API token */
12
+ function buildAuthHeaders(credentials: IntegrationCredentials): Record<string, string> {
13
+ const email = credentials.credentials.email ?? '';
14
+ const apiToken = credentials.credentials.apiToken ?? '';
15
+ if (email && apiToken) {
16
+ const encoded = Buffer.from(`${email}:${apiToken}`).toString('base64');
17
+ return { Authorization: `Basic ${encoded}` };
18
+ }
19
+ return {};
20
+ }
21
+
22
+ // --- REST helper ---
23
+
24
+ async function jiraGet<T>(
25
+ path: string,
26
+ config: IntegrationConfig,
27
+ credentials: IntegrationCredentials,
28
+ fetchFn: FetchFn,
29
+ params?: Record<string, string>,
30
+ ): Promise<T> {
31
+ const base = `${config.endpoint.replace(/\/$/, '')}${path}`;
32
+ const url = new URL(base);
33
+ if (params) {
34
+ for (const [key, value] of Object.entries(params)) {
35
+ url.searchParams.set(key, value);
36
+ }
37
+ }
38
+
39
+ const headers: Record<string, string> = {
40
+ Accept: 'application/json',
41
+ ...buildAuthHeaders(credentials),
42
+ ...config.headers,
43
+ };
44
+
45
+ const res = await fetchFn(url.toString(), { headers });
46
+ if (!res.ok) {
47
+ throw new Error(`Jira API returned ${res.status}: ${res.statusText}`);
48
+ }
49
+ return (await res.json()) as T;
50
+ }
51
+
52
+ // --- Probe handlers ---
53
+
54
+ const issuesSearch: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
55
+ const jql = params?.jql as string;
56
+ if (!jql) throw new Error('jql parameter is required');
57
+
58
+ const maxResults = (params?.max_results as number) || 50;
59
+ const queryParams: Record<string, string> = {
60
+ jql,
61
+ maxResults: String(maxResults),
62
+ };
63
+
64
+ const data = await jiraGet<{
65
+ issues?: Array<{
66
+ key: string;
67
+ fields?: {
68
+ summary?: string;
69
+ status?: { name?: string };
70
+ priority?: { name?: string };
71
+ assignee?: { displayName?: string };
72
+ created?: string;
73
+ updated?: string;
74
+ issuetype?: { name?: string };
75
+ };
76
+ }>;
77
+ total?: number;
78
+ }>('/rest/api/3/search/jql', config, credentials, fetchFn, queryParams);
79
+
80
+ const issues = data.issues ?? [];
81
+ return {
82
+ issues: issues.map((i) => ({
83
+ key: i.key,
84
+ summary: i.fields?.summary ?? null,
85
+ status: i.fields?.status?.name ?? null,
86
+ priority: i.fields?.priority?.name ?? null,
87
+ assignee: i.fields?.assignee?.displayName ?? null,
88
+ issueType: i.fields?.issuetype?.name ?? null,
89
+ created: i.fields?.created ?? null,
90
+ updated: i.fields?.updated ?? null,
91
+ })),
92
+ total: data.total ?? issues.length,
93
+ count: issues.length,
94
+ };
95
+ };
96
+
97
+ const issueDetail: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
98
+ const key = params?.key as string;
99
+ if (!key) throw new Error('key parameter is required (e.g. PROJ-123)');
100
+
101
+ return jiraGet(`/rest/api/3/issue/${key}`, config, credentials, fetchFn);
102
+ };
103
+
104
+ const issueChangelog: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
105
+ const key = params?.key as string;
106
+ if (!key) throw new Error('key parameter is required (e.g. PROJ-123)');
107
+
108
+ const data = await jiraGet<{
109
+ values?: Array<{
110
+ id?: string;
111
+ author?: { displayName?: string };
112
+ created?: string;
113
+ items?: Array<{
114
+ field?: string;
115
+ fromString?: string;
116
+ toString?: string;
117
+ }>;
118
+ }>;
119
+ total?: number;
120
+ }>(`/rest/api/3/issue/${key}/changelog`, config, credentials, fetchFn);
121
+
122
+ const entries = data.values ?? [];
123
+ return {
124
+ changelog: entries.map((e) => ({
125
+ id: e.id ?? null,
126
+ author: e.author?.displayName ?? null,
127
+ created: e.created ?? null,
128
+ items: (e.items ?? []).map((item) => ({
129
+ field: item.field ?? null,
130
+ from: item.fromString ?? null,
131
+ to: item.toString ?? null,
132
+ })),
133
+ })),
134
+ total: data.total ?? entries.length,
135
+ };
136
+ };
137
+
138
+ const projectsList: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
139
+ const data = await jiraGet<{
140
+ values?: Array<{
141
+ key: string;
142
+ name: string;
143
+ projectTypeKey?: string;
144
+ style?: string;
145
+ }>;
146
+ total?: number;
147
+ }>('/rest/api/3/project/search', config, credentials, fetchFn);
148
+
149
+ const projects = data.values ?? [];
150
+ return {
151
+ projects: projects.map((p) => ({
152
+ key: p.key,
153
+ name: p.name,
154
+ projectType: p.projectTypeKey ?? null,
155
+ style: p.style ?? null,
156
+ })),
157
+ count: projects.length,
158
+ };
159
+ };
160
+
161
+ const jiraHealth: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
162
+ const data = await jiraGet<{
163
+ baseUrl?: string;
164
+ version?: string;
165
+ deploymentType?: string;
166
+ serverTitle?: string;
167
+ }>('/rest/api/3/serverInfo', config, credentials, fetchFn);
168
+
169
+ return {
170
+ baseUrl: data.baseUrl ?? null,
171
+ version: data.version ?? null,
172
+ deploymentType: data.deploymentType ?? null,
173
+ serverTitle: data.serverTitle ?? null,
174
+ };
175
+ };
176
+
177
+ // --- Pack definition ---
178
+
179
+ export const jiraPack: IntegrationPack = {
180
+ manifest: {
181
+ name: 'jira',
182
+ type: 'integration',
183
+ version: '0.1.0',
184
+ description: 'Atlassian Jira — issue search, details, changelog, and projects',
185
+ requires: { groups: [], files: [], commands: [] },
186
+ probes: [
187
+ {
188
+ name: 'issues.search',
189
+ description: 'Search issues via JQL query',
190
+ capability: 'observe',
191
+ params: {
192
+ jql: {
193
+ type: 'string',
194
+ description: 'JQL query (e.g. "project = PROJ AND status = Open")',
195
+ required: true,
196
+ },
197
+ max_results: {
198
+ type: 'number',
199
+ description: 'Maximum results to return (default: 50)',
200
+ required: false,
201
+ },
202
+ },
203
+ timeout: 15000,
204
+ },
205
+ {
206
+ name: 'issue.detail',
207
+ description: 'Get full issue details by key',
208
+ capability: 'observe',
209
+ params: {
210
+ key: {
211
+ type: 'string',
212
+ description: 'Issue key (e.g. PROJ-123)',
213
+ required: true,
214
+ },
215
+ },
216
+ timeout: 15000,
217
+ },
218
+ {
219
+ name: 'issue.changelog',
220
+ description: 'Get change history for an issue',
221
+ capability: 'observe',
222
+ params: {
223
+ key: {
224
+ type: 'string',
225
+ description: 'Issue key (e.g. PROJ-123)',
226
+ required: true,
227
+ },
228
+ },
229
+ timeout: 15000,
230
+ },
231
+ {
232
+ name: 'projects.list',
233
+ description: 'List all accessible projects',
234
+ capability: 'observe',
235
+ params: {},
236
+ timeout: 15000,
237
+ },
238
+ {
239
+ name: 'health',
240
+ description: 'Jira server info and connectivity check',
241
+ capability: 'observe',
242
+ params: {},
243
+ timeout: 10000,
244
+ },
245
+ ],
246
+ runbook: {
247
+ category: 'itsm',
248
+ probes: ['health', 'projects.list'],
249
+ parallel: true,
250
+ },
251
+ },
252
+
253
+ handlers: {
254
+ 'issues.search': issuesSearch,
255
+ 'issue.detail': issueDetail,
256
+ 'issue.changelog': issueChangelog,
257
+ 'projects.list': projectsList,
258
+ health: jiraHealth,
259
+ },
260
+
261
+ testConnection: async (config, credentials, fetchFn) => {
262
+ const url = `${config.endpoint.replace(/\/$/, '')}/rest/api/3/serverInfo`;
263
+ const headers: Record<string, string> = {
264
+ Accept: 'application/json',
265
+ ...buildAuthHeaders(credentials),
266
+ ...config.headers,
267
+ };
268
+
269
+ const res = await fetchFn(url, { headers });
270
+ return res.ok;
271
+ },
272
+ };
@@ -0,0 +1,228 @@
1
+ import type {
2
+ FetchFn,
3
+ IntegrationConfig,
4
+ IntegrationCredentials,
5
+ IntegrationPack,
6
+ IntegrationProbeHandler,
7
+ } from '@sonde/shared';
8
+
9
+ // --- Auth helpers ---
10
+
11
+ /** Build auth headers: Basic (username:password) or Bearer token */
12
+ function buildAuthHeaders(credentials: IntegrationCredentials): Record<string, string> {
13
+ if (credentials.authMethod === 'bearer_token') {
14
+ const token = credentials.credentials.token ?? '';
15
+ return { Authorization: `Bearer ${token}` };
16
+ }
17
+
18
+ const { username, password } = credentials.credentials;
19
+ if (username && password) {
20
+ const encoded = Buffer.from(`${username}:${password}`).toString('base64');
21
+ return { Authorization: `Basic ${encoded}` };
22
+ }
23
+ return {};
24
+ }
25
+
26
+ // --- REST helper ---
27
+
28
+ async function lokiGet<T>(
29
+ path: string,
30
+ config: IntegrationConfig,
31
+ credentials: IntegrationCredentials,
32
+ fetchFn: FetchFn,
33
+ params?: Record<string, string>,
34
+ ): Promise<T> {
35
+ const base = `${config.endpoint.replace(/\/$/, '')}${path}`;
36
+ const url = new URL(base);
37
+ if (params) {
38
+ for (const [key, value] of Object.entries(params)) {
39
+ url.searchParams.set(key, value);
40
+ }
41
+ }
42
+
43
+ const headers: Record<string, string> = {
44
+ Accept: 'application/json',
45
+ ...buildAuthHeaders(credentials),
46
+ ...config.headers,
47
+ };
48
+
49
+ const res = await fetchFn(url.toString(), { headers });
50
+ if (!res.ok) {
51
+ throw new Error(`Loki API returned ${res.status}: ${res.statusText}`);
52
+ }
53
+ return (await res.json()) as T;
54
+ }
55
+
56
+ // --- Probe handlers ---
57
+
58
+ const lokiQuery: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
59
+ const query = params?.query as string;
60
+ if (!query) throw new Error('query parameter is required (LogQL expression)');
61
+
62
+ const limit = (params?.limit as number) || 100;
63
+ const now = Math.floor(Date.now() / 1e9);
64
+ const start = params?.start as string | undefined;
65
+ const end = params?.end as string | undefined;
66
+
67
+ const queryParams: Record<string, string> = {
68
+ query,
69
+ limit: String(limit),
70
+ start: start ?? String(now - 3600),
71
+ end: end ?? String(now),
72
+ };
73
+
74
+ const data = await lokiGet<{
75
+ status?: string;
76
+ data?: {
77
+ resultType?: string;
78
+ result?: Array<{
79
+ stream?: Record<string, string>;
80
+ values?: Array<[string, string]>;
81
+ }>;
82
+ };
83
+ }>('/loki/api/v1/query_range', config, credentials, fetchFn, queryParams);
84
+
85
+ const results = data.data?.result ?? [];
86
+ return {
87
+ status: data.status ?? 'unknown',
88
+ resultType: data.data?.resultType ?? null,
89
+ results: results.map((r) => ({
90
+ stream: r.stream ?? {},
91
+ values: (r.values ?? []).map(([ts, line]) => ({
92
+ timestamp: ts,
93
+ line,
94
+ })),
95
+ })),
96
+ count: results.length,
97
+ };
98
+ };
99
+
100
+ const lokiLabels: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
101
+ const data = await lokiGet<{
102
+ status?: string;
103
+ data?: string[];
104
+ }>('/loki/api/v1/labels', config, credentials, fetchFn);
105
+
106
+ return {
107
+ labels: data.data ?? [],
108
+ count: (data.data ?? []).length,
109
+ };
110
+ };
111
+
112
+ const lokiSeries: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
113
+ const match = params?.match as string;
114
+ if (!match) throw new Error('match parameter is required (label selector)');
115
+
116
+ const data = await lokiGet<{
117
+ status?: string;
118
+ data?: Array<Record<string, string>>;
119
+ }>('/loki/api/v1/series', config, credentials, fetchFn, { match });
120
+
121
+ return {
122
+ series: data.data ?? [],
123
+ count: (data.data ?? []).length,
124
+ };
125
+ };
126
+
127
+ const lokiHealth: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
128
+ const url = `${config.endpoint.replace(/\/$/, '')}/ready`;
129
+ const headers: Record<string, string> = {
130
+ ...buildAuthHeaders(credentials),
131
+ ...config.headers,
132
+ };
133
+
134
+ const res = await fetchFn(url, { headers });
135
+ return { ready: res.ok, status: res.status };
136
+ };
137
+
138
+ // --- Pack definition ---
139
+
140
+ export const lokiPack: IntegrationPack = {
141
+ manifest: {
142
+ name: 'loki',
143
+ type: 'integration',
144
+ version: '0.1.0',
145
+ description: 'Grafana Loki — LogQL queries, labels, series, and health',
146
+ requires: { groups: [], files: [], commands: [] },
147
+ probes: [
148
+ {
149
+ name: 'query',
150
+ description: 'Run a LogQL query over a time range',
151
+ capability: 'observe',
152
+ params: {
153
+ query: {
154
+ type: 'string',
155
+ description: 'LogQL query expression (e.g. {job="varlogs"})',
156
+ required: true,
157
+ },
158
+ start: {
159
+ type: 'string',
160
+ description: 'Start time as Unix epoch seconds (default: 1 hour ago)',
161
+ required: false,
162
+ },
163
+ end: {
164
+ type: 'string',
165
+ description: 'End time as Unix epoch seconds (default: now)',
166
+ required: false,
167
+ },
168
+ limit: {
169
+ type: 'number',
170
+ description: 'Maximum log entries to return (default: 100)',
171
+ required: false,
172
+ },
173
+ },
174
+ timeout: 30000,
175
+ },
176
+ {
177
+ name: 'labels',
178
+ description: 'List all label names',
179
+ capability: 'observe',
180
+ params: {},
181
+ timeout: 10000,
182
+ },
183
+ {
184
+ name: 'series',
185
+ description: 'Find series matching a label selector',
186
+ capability: 'observe',
187
+ params: {
188
+ match: {
189
+ type: 'string',
190
+ description: 'Label selector (e.g. {job="varlogs"})',
191
+ required: true,
192
+ },
193
+ },
194
+ timeout: 15000,
195
+ },
196
+ {
197
+ name: 'health',
198
+ description: 'Loki readiness check',
199
+ capability: 'observe',
200
+ params: {},
201
+ timeout: 5000,
202
+ },
203
+ ],
204
+ runbook: {
205
+ category: 'observability',
206
+ probes: ['health', 'labels'],
207
+ parallel: true,
208
+ },
209
+ },
210
+
211
+ handlers: {
212
+ query: lokiQuery,
213
+ labels: lokiLabels,
214
+ series: lokiSeries,
215
+ health: lokiHealth,
216
+ },
217
+
218
+ testConnection: async (config, credentials, fetchFn) => {
219
+ const url = `${config.endpoint.replace(/\/$/, '')}/ready`;
220
+ const headers: Record<string, string> = {
221
+ ...buildAuthHeaders(credentials),
222
+ ...config.headers,
223
+ };
224
+
225
+ const res = await fetchFn(url, { headers });
226
+ return res.ok;
227
+ },
228
+ };