@chirpier/chirpier-js 0.1.5 → 0.2.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 +180 -108
- package/dist/__tests__/chirpier.test.js +395 -96
- package/dist/constants.d.ts +7 -6
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -4
- package/dist/index.d.ts +132 -56
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +478 -193
- package/package.json +2 -2
- package/src/__tests__/chirpier.test.ts +265 -93
- package/src/constants.ts +7 -6
- package/src/index.ts +495 -189
package/src/index.ts
CHANGED
|
@@ -1,181 +1,377 @@
|
|
|
1
|
-
// Import necessary dependencies
|
|
2
1
|
import axios, { AxiosInstance } from "axios";
|
|
3
2
|
import axiosRetry from "axios-retry";
|
|
4
|
-
import
|
|
3
|
+
import dotenv from "dotenv";
|
|
5
4
|
import {
|
|
6
5
|
DEFAULT_RETRIES,
|
|
7
6
|
DEFAULT_TIMEOUT,
|
|
8
7
|
DEFAULT_BATCH_SIZE,
|
|
9
8
|
DEFAULT_FLUSH_DELAY,
|
|
10
9
|
MAX_QUEUE_SIZE,
|
|
10
|
+
DEFAULT_API_ENDPOINT,
|
|
11
|
+
DEFAULT_SERVICER_ENDPOINT,
|
|
11
12
|
} from "./constants";
|
|
12
13
|
import AsyncLock from "async-lock";
|
|
13
14
|
|
|
14
|
-
// Define logging levels
|
|
15
|
-
export enum LogLevel {
|
|
15
|
+
// Define logging levels as const enum for better tree-shaking
|
|
16
|
+
export const enum LogLevel {
|
|
16
17
|
None = 0,
|
|
17
18
|
Error = 1,
|
|
18
19
|
Info = 2,
|
|
19
20
|
Debug = 3,
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export interface Config {
|
|
24
|
+
key?: string;
|
|
25
|
+
apiEndpoint?: string;
|
|
26
|
+
servicerEndpoint?: string;
|
|
26
27
|
logLevel?: LogLevel;
|
|
28
|
+
retries?: number;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
batchSize?: number;
|
|
31
|
+
flushDelay?: number;
|
|
32
|
+
maxQueueSize?: number;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated Use Config.
|
|
37
|
+
*/
|
|
38
|
+
export type Options = Config;
|
|
39
|
+
|
|
40
|
+
export interface Log {
|
|
41
|
+
agent_id?: string;
|
|
42
|
+
event: string;
|
|
33
43
|
value: number;
|
|
44
|
+
meta?: unknown;
|
|
45
|
+
occurred_at?: string | Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EventDefinition {
|
|
49
|
+
readonly event_id: string;
|
|
50
|
+
readonly agent_id?: string;
|
|
51
|
+
readonly event: string;
|
|
52
|
+
readonly title?: string;
|
|
53
|
+
readonly public: boolean;
|
|
54
|
+
readonly description?: string;
|
|
55
|
+
readonly unit?: string;
|
|
56
|
+
readonly semantic_class: string;
|
|
57
|
+
readonly default_aggregate: string;
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
readonly origin: string;
|
|
60
|
+
readonly archived_at?: string;
|
|
61
|
+
readonly created_at?: string;
|
|
34
62
|
}
|
|
35
63
|
|
|
64
|
+
export interface Policy {
|
|
65
|
+
readonly policy_id: string;
|
|
66
|
+
readonly event_id: string;
|
|
67
|
+
readonly title: string;
|
|
68
|
+
readonly description?: string;
|
|
69
|
+
readonly channel: string;
|
|
70
|
+
readonly period: string;
|
|
71
|
+
readonly aggregate: string;
|
|
72
|
+
readonly condition: string;
|
|
73
|
+
readonly threshold: number;
|
|
74
|
+
readonly severity: string;
|
|
75
|
+
readonly enabled: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Alert {
|
|
79
|
+
readonly alert_id: string;
|
|
80
|
+
readonly policy_id: string;
|
|
81
|
+
readonly event_id: string;
|
|
82
|
+
readonly agent_id?: string;
|
|
83
|
+
readonly event: string;
|
|
84
|
+
readonly title: string;
|
|
85
|
+
readonly period: string;
|
|
86
|
+
readonly aggregate: string;
|
|
87
|
+
readonly condition: string;
|
|
88
|
+
readonly threshold: number;
|
|
89
|
+
readonly severity: string;
|
|
90
|
+
readonly status: string;
|
|
91
|
+
readonly value: number;
|
|
92
|
+
readonly count: number;
|
|
93
|
+
readonly min: number;
|
|
94
|
+
readonly max: number;
|
|
95
|
+
readonly triggered_at?: string;
|
|
96
|
+
readonly acknowledged_at?: string;
|
|
97
|
+
readonly resolved_at?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AlertDelivery {
|
|
101
|
+
readonly attempt_id: string;
|
|
102
|
+
readonly alert_id: string;
|
|
103
|
+
readonly webhook_id?: string;
|
|
104
|
+
readonly channel: string;
|
|
105
|
+
readonly target: string;
|
|
106
|
+
readonly status: string;
|
|
107
|
+
readonly response_status?: number;
|
|
108
|
+
readonly error_message?: string;
|
|
109
|
+
readonly created_at: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface EventLogPoint {
|
|
113
|
+
readonly event_id: string;
|
|
114
|
+
readonly agent_id?: string;
|
|
115
|
+
readonly event: string;
|
|
116
|
+
readonly period: string;
|
|
117
|
+
readonly occurred_at: string;
|
|
118
|
+
readonly count: number;
|
|
119
|
+
readonly value: number;
|
|
120
|
+
readonly squares: number;
|
|
121
|
+
readonly min: number;
|
|
122
|
+
readonly max: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface PaginationOptions {
|
|
126
|
+
period?: "minute" | "hour" | "day";
|
|
127
|
+
limit?: number;
|
|
128
|
+
offset?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type DeliveryKind = "alert" | "test" | "all";
|
|
132
|
+
|
|
36
133
|
// Custom error class for Chirpier-specific errors
|
|
37
134
|
export class ChirpierError extends Error {
|
|
38
|
-
constructor(
|
|
135
|
+
constructor(
|
|
136
|
+
message: string,
|
|
137
|
+
public readonly code?: string
|
|
138
|
+
) {
|
|
39
139
|
super(message);
|
|
40
140
|
this.name = "ChirpierError";
|
|
41
141
|
Object.setPrototypeOf(this, ChirpierError.prototype);
|
|
42
142
|
}
|
|
43
143
|
}
|
|
44
144
|
|
|
45
|
-
interface
|
|
46
|
-
|
|
47
|
-
timestamp: number;
|
|
145
|
+
interface QueuedLog {
|
|
146
|
+
readonly log: Log;
|
|
147
|
+
readonly timestamp: number;
|
|
48
148
|
retryCount: number;
|
|
49
149
|
}
|
|
50
150
|
|
|
51
|
-
|
|
52
|
-
* Main Chirpier class for monitoring events.
|
|
53
|
-
*/
|
|
54
|
-
export class Chirpier {
|
|
55
|
-
private static instance: Chirpier | null = null;
|
|
151
|
+
export class Client {
|
|
56
152
|
private readonly apiKey: string;
|
|
57
153
|
private readonly apiEndpoint: string;
|
|
154
|
+
private readonly servicerEndpoint: string;
|
|
58
155
|
private readonly retries: number;
|
|
59
156
|
private readonly timeout: number;
|
|
60
157
|
private readonly axiosInstance: AxiosInstance;
|
|
61
|
-
private
|
|
158
|
+
private logQueue: QueuedLog[] = [];
|
|
62
159
|
private readonly batchSize: number;
|
|
63
160
|
private readonly flushDelay: number;
|
|
161
|
+
private readonly maxQueueSize: number;
|
|
64
162
|
private flushTimeoutId: NodeJS.Timeout | null = null;
|
|
65
|
-
private readonly queueLock
|
|
66
|
-
private readonly flushLock
|
|
163
|
+
private readonly queueLock: AsyncLock;
|
|
164
|
+
private readonly flushLock: AsyncLock;
|
|
67
165
|
private readonly logLevel: LogLevel;
|
|
68
166
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
167
|
+
constructor(options: Config = {}) {
|
|
168
|
+
const {
|
|
169
|
+
key: providedKey,
|
|
170
|
+
apiEndpoint = DEFAULT_API_ENDPOINT,
|
|
171
|
+
servicerEndpoint = DEFAULT_SERVICER_ENDPOINT,
|
|
172
|
+
logLevel = LogLevel.None,
|
|
173
|
+
retries = DEFAULT_RETRIES,
|
|
174
|
+
timeout = DEFAULT_TIMEOUT,
|
|
175
|
+
batchSize = DEFAULT_BATCH_SIZE,
|
|
176
|
+
flushDelay = DEFAULT_FLUSH_DELAY,
|
|
177
|
+
maxQueueSize = MAX_QUEUE_SIZE,
|
|
178
|
+
} = options;
|
|
179
|
+
|
|
180
|
+
const key = resolveAPIKey(providedKey);
|
|
181
|
+
|
|
182
|
+
if (!key) {
|
|
183
|
+
throw new ChirpierError("API key is required", "INVALID_KEY");
|
|
184
|
+
}
|
|
75
185
|
|
|
76
|
-
if (!key
|
|
77
|
-
throw new ChirpierError("API key
|
|
186
|
+
if (!isValidAPIKey(key)) {
|
|
187
|
+
throw new ChirpierError("Invalid API key: must start with 'chp_'", "INVALID_KEY");
|
|
78
188
|
}
|
|
79
189
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
190
|
+
if (apiEndpoint !== undefined) {
|
|
191
|
+
if (typeof apiEndpoint !== "string" || apiEndpoint.trim().length === 0) {
|
|
192
|
+
throw new ChirpierError(
|
|
193
|
+
"apiEndpoint must be a non-empty string",
|
|
194
|
+
"INVALID_API_ENDPOINT"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let parsedURL: URL;
|
|
199
|
+
try {
|
|
200
|
+
parsedURL = new URL(apiEndpoint);
|
|
201
|
+
} catch {
|
|
202
|
+
throw new ChirpierError(
|
|
203
|
+
"apiEndpoint must be a valid absolute URL",
|
|
204
|
+
"INVALID_API_ENDPOINT"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (parsedURL.protocol !== "https:" && parsedURL.protocol !== "http:") {
|
|
209
|
+
throw new ChirpierError(
|
|
210
|
+
"apiEndpoint must use http or https",
|
|
211
|
+
"INVALID_API_ENDPOINT"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
82
214
|
}
|
|
83
215
|
|
|
84
|
-
|
|
216
|
+
// Validate numeric options
|
|
217
|
+
if (retries < 0 || !Number.isInteger(retries)) {
|
|
218
|
+
throw new ChirpierError("Retries must be a non-negative integer", "INVALID_RETRIES");
|
|
219
|
+
}
|
|
220
|
+
if (timeout <= 0) {
|
|
221
|
+
throw new ChirpierError("Timeout must be positive", "INVALID_TIMEOUT");
|
|
222
|
+
}
|
|
223
|
+
if (batchSize <= 0 || !Number.isInteger(batchSize)) {
|
|
224
|
+
throw new ChirpierError("Batch size must be a positive integer", "INVALID_BATCH_SIZE");
|
|
225
|
+
}
|
|
226
|
+
if (flushDelay < 0) {
|
|
227
|
+
throw new ChirpierError("Flush delay must be non-negative", "INVALID_FLUSH_DELAY");
|
|
228
|
+
}
|
|
229
|
+
if (maxQueueSize <= 0 || !Number.isInteger(maxQueueSize)) {
|
|
230
|
+
throw new ChirpierError("Max queue size must be a positive integer", "INVALID_QUEUE_SIZE");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.apiEndpoint = apiEndpoint ?? DEFAULT_API_ENDPOINT;
|
|
234
|
+
this.servicerEndpoint = servicerEndpoint ?? DEFAULT_SERVICER_ENDPOINT;
|
|
85
235
|
this.apiKey = key;
|
|
86
|
-
this.retries =
|
|
87
|
-
this.timeout =
|
|
88
|
-
this.batchSize =
|
|
89
|
-
this.flushDelay =
|
|
236
|
+
this.retries = retries;
|
|
237
|
+
this.timeout = timeout;
|
|
238
|
+
this.batchSize = batchSize;
|
|
239
|
+
this.flushDelay = flushDelay;
|
|
240
|
+
this.maxQueueSize = maxQueueSize;
|
|
90
241
|
this.logLevel = logLevel;
|
|
91
242
|
|
|
92
|
-
|
|
243
|
+
this.queueLock = new AsyncLock({ maxPending: this.maxQueueSize });
|
|
244
|
+
this.flushLock = new AsyncLock({ maxPending: this.maxQueueSize });
|
|
245
|
+
|
|
93
246
|
this.axiosInstance = axios.create({
|
|
94
247
|
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
95
248
|
timeout: this.timeout,
|
|
96
249
|
});
|
|
97
250
|
|
|
98
|
-
// Add the interceptor here
|
|
99
251
|
this.axiosInstance.interceptors.response.use(
|
|
100
252
|
(response) => response,
|
|
101
|
-
(error) =>
|
|
102
|
-
// Don't handle the error here; let axios-retry handle it
|
|
103
|
-
return Promise.reject(error);
|
|
104
|
-
}
|
|
253
|
+
(error) => Promise.reject(error)
|
|
105
254
|
);
|
|
106
255
|
|
|
107
|
-
// Apply axios-retry to your Axios instance
|
|
108
256
|
axiosRetry(this.axiosInstance, {
|
|
109
257
|
retries: this.retries,
|
|
110
258
|
retryDelay: (retryCount) => {
|
|
111
|
-
|
|
259
|
+
const baseDelay = Math.pow(2, retryCount) * 1000;
|
|
260
|
+
const jitter = Math.random() * 0.3 * baseDelay;
|
|
261
|
+
return baseDelay + jitter;
|
|
112
262
|
},
|
|
113
263
|
retryCondition: (error) => {
|
|
114
|
-
// Retry on network errors, 5xx errors, and 429 (Too Many Requests)
|
|
115
264
|
return (
|
|
116
|
-
axiosRetry.isNetworkError(error) ||
|
|
265
|
+
axiosRetry.isNetworkError(error) ||
|
|
117
266
|
axiosRetry.isRetryableError(error) ||
|
|
118
|
-
(
|
|
267
|
+
(error.response && error.response.status) === 429
|
|
119
268
|
);
|
|
120
269
|
},
|
|
121
270
|
shouldResetTimeout: true,
|
|
122
271
|
});
|
|
123
272
|
}
|
|
124
273
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Chirpier.instance = new Chirpier(options);
|
|
274
|
+
private isValidLog(log: Log): boolean {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const oldestAllowed = now - 30 * 24 * 60 * 60 * 1000;
|
|
277
|
+
const newestAllowed = now + 24 * 60 * 60 * 1000;
|
|
278
|
+
|
|
279
|
+
if (typeof log.event !== "string" || log.event.trim().length === 0) {
|
|
280
|
+
return false;
|
|
133
281
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
282
|
+
|
|
283
|
+
if (typeof log.value !== "number" || !Number.isFinite(log.value)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (log.agent_id !== undefined && typeof log.agent_id !== "string") {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (log.meta !== undefined) {
|
|
292
|
+
try {
|
|
293
|
+
const serializedMeta = JSON.stringify(log.meta);
|
|
294
|
+
if (serializedMeta === undefined) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (log.occurred_at !== undefined) {
|
|
303
|
+
const occurredAtMillis =
|
|
304
|
+
log.occurred_at instanceof Date
|
|
305
|
+
? log.occurred_at.getTime()
|
|
306
|
+
: new Date(log.occurred_at).getTime();
|
|
307
|
+
|
|
308
|
+
if (!Number.isFinite(occurredAtMillis)) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (occurredAtMillis < oldestAllowed || occurredAtMillis > newestAllowed) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return true;
|
|
153
318
|
}
|
|
154
319
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
320
|
+
private normalizeLog(log: Log): Log {
|
|
321
|
+
const normalizedLog: Log = {
|
|
322
|
+
event: log.event.trim(),
|
|
323
|
+
value: log.value,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (typeof log.agent_id === "string") {
|
|
327
|
+
const trimmedAgentID = log.agent_id.trim();
|
|
328
|
+
if (trimmedAgentID.length > 0) {
|
|
329
|
+
normalizedLog.agent_id = trimmedAgentID;
|
|
163
330
|
}
|
|
164
|
-
return; // Silently drop the event
|
|
165
331
|
}
|
|
166
332
|
|
|
333
|
+
if (log.meta !== undefined) {
|
|
334
|
+
normalizedLog.meta = log.meta;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (log.occurred_at !== undefined) {
|
|
338
|
+
const occurredAtDate =
|
|
339
|
+
log.occurred_at instanceof Date ? log.occurred_at : new Date(log.occurred_at);
|
|
340
|
+
normalizedLog.occurred_at = occurredAtDate.toISOString();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return normalizedLog;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public async log(log: Log): Promise<void> {
|
|
347
|
+
if (!this.isValidLog(log)) {
|
|
348
|
+
throw new ChirpierError(
|
|
349
|
+
"Invalid log format: event must not be empty, value must be a finite number, agent_id must be a string when provided, meta must be JSON-encodable, and occurred_at must be within the last 30 days and no more than 1 day in the future",
|
|
350
|
+
"INVALID_LOG"
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const normalizedLog = this.normalizeLog(log);
|
|
355
|
+
|
|
356
|
+
let queueFull = false;
|
|
357
|
+
|
|
167
358
|
await this.queueLock.acquire("queue", async () => {
|
|
168
|
-
if (this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return; // Silently drop the event
|
|
173
|
-
}
|
|
359
|
+
if (this.logQueue.length >= this.maxQueueSize) {
|
|
360
|
+
queueFull = true;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
174
363
|
|
|
175
|
-
this.
|
|
364
|
+
this.logQueue.push({ log: normalizedLog, timestamp: Date.now(), retryCount: 0 });
|
|
176
365
|
});
|
|
177
366
|
|
|
178
|
-
if (
|
|
367
|
+
if (queueFull) {
|
|
368
|
+
throw new ChirpierError(
|
|
369
|
+
`Log queue is full (max size: ${this.maxQueueSize})`,
|
|
370
|
+
"QUEUE_FULL"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (this.logQueue.length >= this.batchSize) {
|
|
179
375
|
await this.flushQueue();
|
|
180
376
|
} else if (!this.flushTimeoutId) {
|
|
181
377
|
this.flushTimeoutId = setTimeout(
|
|
@@ -185,142 +381,238 @@ export class Chirpier {
|
|
|
185
381
|
}
|
|
186
382
|
}
|
|
187
383
|
|
|
188
|
-
/**
|
|
189
|
-
* Flushes the event queue by sending all events to the API.
|
|
190
|
-
*/
|
|
191
384
|
private async flushQueue(): Promise<void> {
|
|
192
|
-
// Acquire the flush lock
|
|
193
385
|
await this.flushLock.acquire("flush", async () => {
|
|
194
|
-
let
|
|
386
|
+
let logsToSend: QueuedLog[] = [];
|
|
195
387
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.eventQueue = [];
|
|
388
|
+
await this.queueLock.acquire("logQueue", async () => {
|
|
389
|
+
if (this.logQueue.length > 0) {
|
|
390
|
+
logsToSend = [...this.logQueue];
|
|
391
|
+
this.logQueue = [];
|
|
201
392
|
}
|
|
202
393
|
});
|
|
203
394
|
|
|
204
|
-
if (
|
|
395
|
+
if (logsToSend.length === 0) {
|
|
205
396
|
return;
|
|
206
397
|
}
|
|
207
398
|
|
|
208
399
|
try {
|
|
209
|
-
// Clear any pending flush timeout
|
|
210
400
|
if (this.flushTimeoutId) {
|
|
211
401
|
clearTimeout(this.flushTimeoutId);
|
|
212
402
|
this.flushTimeoutId = null;
|
|
213
403
|
}
|
|
214
404
|
|
|
215
|
-
|
|
216
|
-
await this.sendEvents(eventsToSend.map((qe) => qe.event));
|
|
405
|
+
await this.sendLogs(logsToSend.map((queuedLog) => queuedLog.log));
|
|
217
406
|
|
|
218
407
|
if (this.logLevel >= LogLevel.Info) {
|
|
219
|
-
console.info(`Successfully sent ${
|
|
408
|
+
console.info(`Successfully sent ${logsToSend.length} logs`);
|
|
220
409
|
}
|
|
221
410
|
} catch (error) {
|
|
222
|
-
// Log failure
|
|
223
411
|
if (this.logLevel >= LogLevel.Error) {
|
|
224
|
-
console.error("Failed to send
|
|
412
|
+
console.error("Failed to send logs:", error);
|
|
225
413
|
}
|
|
226
414
|
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
if (queuedEvent.retryCount >= this.retries) {
|
|
415
|
+
const retryableLogs: QueuedLog[] = [];
|
|
416
|
+
for (const queuedLog of logsToSend) {
|
|
417
|
+
if (queuedLog.retryCount >= this.retries) {
|
|
231
418
|
if (this.logLevel >= LogLevel.Error) {
|
|
232
419
|
console.error(
|
|
233
|
-
`Dropping
|
|
234
|
-
|
|
420
|
+
`Dropping log after ${this.retries} retries:`,
|
|
421
|
+
queuedLog.log
|
|
235
422
|
);
|
|
236
423
|
}
|
|
237
|
-
continue;
|
|
424
|
+
continue;
|
|
238
425
|
}
|
|
239
426
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
retryableEvents.push(queuedEvent);
|
|
427
|
+
queuedLog.retryCount++;
|
|
428
|
+
retryableLogs.push(queuedLog);
|
|
243
429
|
}
|
|
244
430
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.eventQueue = [...retryableEvents, ...this.eventQueue];
|
|
431
|
+
await this.queueLock.acquire("logQueue", async () => {
|
|
432
|
+
this.logQueue = [...retryableLogs, ...this.logQueue];
|
|
248
433
|
});
|
|
249
434
|
}
|
|
250
435
|
});
|
|
251
436
|
}
|
|
252
437
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* @param events - The array of events to send.
|
|
256
|
-
*/
|
|
257
|
-
private async sendEvents(events: Event[]): Promise<void> {
|
|
258
|
-
await this.axiosInstance.post(this.apiEndpoint, events);
|
|
438
|
+
private async sendLogs(logs: Log[]): Promise<void> {
|
|
439
|
+
await this.axiosInstance.post(this.apiEndpoint, logs);
|
|
259
440
|
}
|
|
260
441
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
clearTimeout(
|
|
268
|
-
|
|
442
|
+
public async flush(): Promise<void> {
|
|
443
|
+
await this.flushQueue();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
public async shutdown(): Promise<void> {
|
|
447
|
+
if (this.flushTimeoutId) {
|
|
448
|
+
clearTimeout(this.flushTimeoutId);
|
|
449
|
+
this.flushTimeoutId = null;
|
|
269
450
|
}
|
|
270
|
-
|
|
271
|
-
await
|
|
272
|
-
|
|
273
|
-
|
|
451
|
+
|
|
452
|
+
await this.flushQueue();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
public async close(): Promise<void> {
|
|
456
|
+
await this.shutdown();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public async listEvents(): Promise<EventDefinition[]> {
|
|
460
|
+
const response = await this.axiosInstance.get<EventDefinition[]>(`${this.servicerEndpoint}/events`);
|
|
461
|
+
return response.data;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
public async getEvent(eventID: string): Promise<EventDefinition> {
|
|
465
|
+
const response = await this.axiosInstance.get<EventDefinition>(`${this.servicerEndpoint}/events/${eventID}`);
|
|
466
|
+
return response.data;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
public async updateEvent(
|
|
470
|
+
eventID: string,
|
|
471
|
+
payload: Partial<Omit<EventDefinition, "event_id" | "created_at">>
|
|
472
|
+
): Promise<EventDefinition> {
|
|
473
|
+
const response = await this.axiosInstance.put<EventDefinition>(
|
|
474
|
+
`${this.servicerEndpoint}/events/${eventID}`,
|
|
475
|
+
payload
|
|
476
|
+
);
|
|
477
|
+
return response.data;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
public async listPolicies(): Promise<Policy[]> {
|
|
481
|
+
const response = await this.axiosInstance.get<Policy[]>(`${this.servicerEndpoint}/policies`);
|
|
482
|
+
return response.data;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
public async createPolicy(payload: Omit<Policy, "policy_id">): Promise<Policy> {
|
|
486
|
+
const response = await this.axiosInstance.post<Policy>(`${this.servicerEndpoint}/policies`, payload);
|
|
487
|
+
return response.data;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
public async listAlerts(status?: string): Promise<Alert[]> {
|
|
491
|
+
const endpoint = status
|
|
492
|
+
? `${this.servicerEndpoint}/alerts?status=${encodeURIComponent(status)}`
|
|
493
|
+
: `${this.servicerEndpoint}/alerts`;
|
|
494
|
+
const response = await this.axiosInstance.get<Alert[]>(endpoint);
|
|
495
|
+
return response.data;
|
|
274
496
|
}
|
|
497
|
+
|
|
498
|
+
public async getAlertDeliveries(alertID: string, options: { limit?: number; offset?: number; kind?: DeliveryKind } = {}): Promise<AlertDelivery[]> {
|
|
499
|
+
const params = new URLSearchParams();
|
|
500
|
+
if (options.kind) {
|
|
501
|
+
params.set("kind", options.kind);
|
|
502
|
+
}
|
|
503
|
+
if (typeof options.limit === "number") {
|
|
504
|
+
params.set("limit", String(options.limit));
|
|
505
|
+
}
|
|
506
|
+
if (typeof options.offset === "number") {
|
|
507
|
+
params.set("offset", String(options.offset));
|
|
508
|
+
}
|
|
509
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
510
|
+
const response = await this.axiosInstance.get<AlertDelivery[]>(`${this.servicerEndpoint}/alerts/${alertID}/deliveries${suffix}`);
|
|
511
|
+
return response.data;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public async acknowledgeAlert(alertID: string): Promise<Alert> {
|
|
515
|
+
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/acknowledge`);
|
|
516
|
+
return response.data;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
public async archiveAlert(alertID: string): Promise<Alert> {
|
|
520
|
+
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/archive`);
|
|
521
|
+
return response.data;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
public async testWebhook(webhookID: string): Promise<void> {
|
|
525
|
+
await this.axiosInstance.post(`${this.servicerEndpoint}/webhooks/${webhookID}/test`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public async getEventLogs(eventID: string, options: PaginationOptions = {}): Promise<EventLogPoint[]> {
|
|
529
|
+
const params = new URLSearchParams();
|
|
530
|
+
if (options.period) {
|
|
531
|
+
params.set("period", options.period);
|
|
532
|
+
}
|
|
533
|
+
if (typeof options.limit === "number") {
|
|
534
|
+
params.set("limit", String(options.limit));
|
|
535
|
+
}
|
|
536
|
+
if (typeof options.offset === "number") {
|
|
537
|
+
params.set("offset", String(options.offset));
|
|
538
|
+
}
|
|
539
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
540
|
+
const response = await this.axiosInstance.get<EventLogPoint[]>(`${this.servicerEndpoint}/events/${eventID}/logs${suffix}`);
|
|
541
|
+
return response.data;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
public async resolveAlert(alertID: string): Promise<Alert> {
|
|
545
|
+
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/resolve`);
|
|
546
|
+
return response.data;
|
|
547
|
+
}
|
|
548
|
+
|
|
275
549
|
}
|
|
276
550
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
*/
|
|
282
|
-
function base64UrlDecode(str: string): string {
|
|
283
|
-
// Replace '-' with '+' and '_' with '/'
|
|
284
|
-
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
285
|
-
// Pad the base64 string
|
|
286
|
-
const padding = base64.length % 4;
|
|
287
|
-
if (padding !== 0) {
|
|
288
|
-
base64 += "=".repeat(4 - padding);
|
|
289
|
-
}
|
|
290
|
-
return Base64.decode(base64);
|
|
551
|
+
let instance: Client | null = null;
|
|
552
|
+
|
|
553
|
+
export function createClient(config: Config = {}): Client {
|
|
554
|
+
return new Client(config);
|
|
291
555
|
}
|
|
292
556
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
557
|
+
function isNodeEnvironment(): boolean {
|
|
558
|
+
return typeof process !== "undefined" && !!(process.versions && process.versions.node);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isValidAPIKey(token: string): boolean {
|
|
562
|
+
return token.startsWith("chp_") && token.length > "chp_".length;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function loadDotEnvKey(): string | undefined {
|
|
566
|
+
if (!isNodeEnvironment()) {
|
|
567
|
+
return undefined;
|
|
302
568
|
}
|
|
569
|
+
|
|
303
570
|
try {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
571
|
+
dotenv.config({ path: ".env", override: false });
|
|
572
|
+
} catch {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const envKey = process.env.CHIRPIER_API_KEY;
|
|
577
|
+
if (typeof envKey !== "string") {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const trimmedKey = envKey.trim();
|
|
582
|
+
return trimmedKey.length > 0 ? trimmedKey : undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function resolveAPIKey(providedKey?: string): string | undefined {
|
|
586
|
+
if (typeof providedKey === "string" && providedKey.trim().length > 0) {
|
|
587
|
+
return providedKey.trim();
|
|
310
588
|
}
|
|
589
|
+
|
|
590
|
+
if (typeof process !== "undefined" && process.env && typeof process.env.CHIRPIER_API_KEY === "string") {
|
|
591
|
+
const envKey = process.env.CHIRPIER_API_KEY.trim();
|
|
592
|
+
if (envKey.length > 0) {
|
|
593
|
+
return envKey;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return loadDotEnvKey();
|
|
311
598
|
}
|
|
312
599
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
600
|
+
export function initialize(options: Config = {}): void {
|
|
601
|
+
const resolvedKey = resolveAPIKey(options.key);
|
|
602
|
+
if (!resolvedKey) {
|
|
603
|
+
throw new ChirpierError("API key is required", "INVALID_KEY");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!isValidAPIKey(resolvedKey)) {
|
|
607
|
+
throw new ChirpierError("Invalid API key: must start with 'chp_'", "INVALID_KEY");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (instance) {
|
|
611
|
+
return;
|
|
320
612
|
}
|
|
321
613
|
|
|
322
614
|
try {
|
|
323
|
-
|
|
615
|
+
instance = new Client({ ...options, key: resolvedKey });
|
|
324
616
|
} catch (error) {
|
|
325
617
|
if (error instanceof ChirpierError) {
|
|
326
618
|
if (options.logLevel && options.logLevel >= LogLevel.Error) {
|
|
@@ -338,19 +630,33 @@ export function initialize(options: Options): void {
|
|
|
338
630
|
}
|
|
339
631
|
}
|
|
340
632
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
633
|
+
export async function logEvent(log: Log): Promise<void> {
|
|
634
|
+
if (!instance) {
|
|
635
|
+
throw new ChirpierError(
|
|
636
|
+
"Chirpier SDK is not initialized. Please call initialize() first.",
|
|
637
|
+
"NOT_INITIALIZED"
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
await instance.log(log);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export async function stop(): Promise<void> {
|
|
645
|
+
if (!instance) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
await instance.shutdown();
|
|
650
|
+
instance = null;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export async function flush(): Promise<void> {
|
|
347
654
|
if (!instance) {
|
|
348
655
|
throw new ChirpierError(
|
|
349
|
-
"Chirpier SDK is not initialized. Please call initialize() first."
|
|
656
|
+
"Chirpier SDK is not initialized. Please call initialize() first.",
|
|
657
|
+
"NOT_INITIALIZED"
|
|
350
658
|
);
|
|
351
659
|
}
|
|
352
660
|
|
|
353
|
-
instance.
|
|
354
|
-
console.error("Error in monitor function:", error);
|
|
355
|
-
});
|
|
661
|
+
await instance.flush();
|
|
356
662
|
}
|