@chirpier/chirpier-js 0.2.0 → 0.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 +49 -18
- package/dist/__tests__/chirpier.test.js +150 -14
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +77 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +177 -54
- package/package.json +4 -4
- package/src/__tests__/chirpier.test.ts +105 -11
- package/src/index.ts +173 -70
package/src/index.ts
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
DEFAULT_TIMEOUT,
|
|
7
7
|
DEFAULT_BATCH_SIZE,
|
|
8
8
|
DEFAULT_FLUSH_DELAY,
|
|
9
|
-
MAX_QUEUE_SIZE,
|
|
10
9
|
DEFAULT_API_ENDPOINT,
|
|
11
10
|
DEFAULT_SERVICER_ENDPOINT,
|
|
12
11
|
} from "./constants";
|
|
13
12
|
import AsyncLock from "async-lock";
|
|
13
|
+
import { v7 as uuidv7 } from "uuid";
|
|
14
14
|
|
|
15
15
|
// Define logging levels as const enum for better tree-shaking
|
|
16
16
|
export const enum LogLevel {
|
|
@@ -38,29 +38,36 @@ export interface Config {
|
|
|
38
38
|
export type Options = Config;
|
|
39
39
|
|
|
40
40
|
export interface Log {
|
|
41
|
-
|
|
41
|
+
log_id?: string;
|
|
42
|
+
agent?: string;
|
|
42
43
|
event: string;
|
|
43
44
|
value: number;
|
|
44
45
|
meta?: unknown;
|
|
45
46
|
occurred_at?: string | Date;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
export interface
|
|
49
|
+
export interface Event {
|
|
49
50
|
readonly event_id: string;
|
|
50
|
-
readonly
|
|
51
|
+
readonly agent?: string;
|
|
51
52
|
readonly event: string;
|
|
52
53
|
readonly title?: string;
|
|
53
54
|
readonly public: boolean;
|
|
54
55
|
readonly description?: string;
|
|
55
56
|
readonly unit?: string;
|
|
56
|
-
readonly
|
|
57
|
-
readonly default_aggregate: string;
|
|
58
|
-
readonly enabled: boolean;
|
|
59
|
-
readonly origin: string;
|
|
60
|
-
readonly archived_at?: string;
|
|
57
|
+
readonly timezone: string;
|
|
61
58
|
readonly created_at?: string;
|
|
62
59
|
}
|
|
63
60
|
|
|
61
|
+
export interface CreateEventPayload {
|
|
62
|
+
agent?: string;
|
|
63
|
+
event: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
public?: boolean;
|
|
66
|
+
description?: string;
|
|
67
|
+
unit?: string;
|
|
68
|
+
timezone?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
export interface Policy {
|
|
65
72
|
readonly policy_id: string;
|
|
66
73
|
readonly event_id: string;
|
|
@@ -75,11 +82,15 @@ export interface Policy {
|
|
|
75
82
|
readonly enabled: boolean;
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
export type CreatePolicyPayload = Omit<Policy, "policy_id">;
|
|
86
|
+
|
|
87
|
+
export type UpdatePolicyPayload = Partial<Omit<Policy, "policy_id">>;
|
|
88
|
+
|
|
78
89
|
export interface Alert {
|
|
79
90
|
readonly alert_id: string;
|
|
80
91
|
readonly policy_id: string;
|
|
81
92
|
readonly event_id: string;
|
|
82
|
-
readonly
|
|
93
|
+
readonly agent?: string;
|
|
83
94
|
readonly event: string;
|
|
84
95
|
readonly title: string;
|
|
85
96
|
readonly period: string;
|
|
@@ -100,7 +111,7 @@ export interface Alert {
|
|
|
100
111
|
export interface AlertDelivery {
|
|
101
112
|
readonly attempt_id: string;
|
|
102
113
|
readonly alert_id: string;
|
|
103
|
-
readonly
|
|
114
|
+
readonly destination_id?: string;
|
|
104
115
|
readonly channel: string;
|
|
105
116
|
readonly target: string;
|
|
106
117
|
readonly status: string;
|
|
@@ -109,9 +120,23 @@ export interface AlertDelivery {
|
|
|
109
120
|
readonly created_at: string;
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
export interface Destination {
|
|
124
|
+
readonly destination_id: string;
|
|
125
|
+
readonly channel: string;
|
|
126
|
+
readonly url?: string;
|
|
127
|
+
readonly credentials?: Record<string, unknown>;
|
|
128
|
+
readonly scope: string;
|
|
129
|
+
readonly policy_ids?: string[];
|
|
130
|
+
readonly enabled: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CreateDestinationPayload = Omit<Destination, "destination_id">;
|
|
134
|
+
|
|
135
|
+
export type UpdateDestinationPayload = Partial<Omit<Destination, "destination_id">>;
|
|
136
|
+
|
|
112
137
|
export interface EventLogPoint {
|
|
113
138
|
readonly event_id: string;
|
|
114
|
-
readonly
|
|
139
|
+
readonly agent?: string;
|
|
115
140
|
readonly event: string;
|
|
116
141
|
readonly period: string;
|
|
117
142
|
readonly occurred_at: string;
|
|
@@ -122,6 +147,43 @@ export interface EventLogPoint {
|
|
|
122
147
|
readonly max: number;
|
|
123
148
|
}
|
|
124
149
|
|
|
150
|
+
export interface AnalyticsWindowQuery {
|
|
151
|
+
view: "window";
|
|
152
|
+
period: "1h" | "1d" | "7d" | "1m";
|
|
153
|
+
previous: "previous_window" | "previous_1d" | "previous_7d" | "previous_1m";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface AnalyticsWindowData {
|
|
157
|
+
readonly current_value: number;
|
|
158
|
+
readonly current_count: number;
|
|
159
|
+
readonly previous_value: number;
|
|
160
|
+
readonly previous_count: number;
|
|
161
|
+
readonly value_delta: number;
|
|
162
|
+
readonly count_delta: number;
|
|
163
|
+
readonly value_pct_change: number;
|
|
164
|
+
readonly count_pct_change: number;
|
|
165
|
+
readonly current_mean: number;
|
|
166
|
+
readonly previous_mean: number;
|
|
167
|
+
readonly mean_delta: number;
|
|
168
|
+
readonly mean_pct_change: number;
|
|
169
|
+
readonly current_stddev: number;
|
|
170
|
+
readonly previous_stddev: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface AnalyticsWindowResponse {
|
|
174
|
+
readonly event_id: string;
|
|
175
|
+
readonly view: "window";
|
|
176
|
+
readonly period: "1h" | "1d" | "7d" | "1m";
|
|
177
|
+
readonly previous: "previous_window" | "previous_1d" | "previous_7d" | "previous_1m";
|
|
178
|
+
readonly data: AnalyticsWindowData | null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface DestinationTestResult {
|
|
182
|
+
readonly alert_id: string;
|
|
183
|
+
readonly destination_id: string;
|
|
184
|
+
readonly status: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
125
187
|
export interface PaginationOptions {
|
|
126
188
|
period?: "minute" | "hour" | "day";
|
|
127
189
|
limit?: number;
|
|
@@ -145,7 +207,6 @@ export class ChirpierError extends Error {
|
|
|
145
207
|
interface QueuedLog {
|
|
146
208
|
readonly log: Log;
|
|
147
209
|
readonly timestamp: number;
|
|
148
|
-
retryCount: number;
|
|
149
210
|
}
|
|
150
211
|
|
|
151
212
|
export class Client {
|
|
@@ -158,7 +219,6 @@ export class Client {
|
|
|
158
219
|
private logQueue: QueuedLog[] = [];
|
|
159
220
|
private readonly batchSize: number;
|
|
160
221
|
private readonly flushDelay: number;
|
|
161
|
-
private readonly maxQueueSize: number;
|
|
162
222
|
private flushTimeoutId: NodeJS.Timeout | null = null;
|
|
163
223
|
private readonly queueLock: AsyncLock;
|
|
164
224
|
private readonly flushLock: AsyncLock;
|
|
@@ -174,7 +234,7 @@ export class Client {
|
|
|
174
234
|
timeout = DEFAULT_TIMEOUT,
|
|
175
235
|
batchSize = DEFAULT_BATCH_SIZE,
|
|
176
236
|
flushDelay = DEFAULT_FLUSH_DELAY,
|
|
177
|
-
maxQueueSize
|
|
237
|
+
maxQueueSize,
|
|
178
238
|
} = options;
|
|
179
239
|
|
|
180
240
|
const key = resolveAPIKey(providedKey);
|
|
@@ -226,10 +286,6 @@ export class Client {
|
|
|
226
286
|
if (flushDelay < 0) {
|
|
227
287
|
throw new ChirpierError("Flush delay must be non-negative", "INVALID_FLUSH_DELAY");
|
|
228
288
|
}
|
|
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
289
|
this.apiEndpoint = apiEndpoint ?? DEFAULT_API_ENDPOINT;
|
|
234
290
|
this.servicerEndpoint = servicerEndpoint ?? DEFAULT_SERVICER_ENDPOINT;
|
|
235
291
|
this.apiKey = key;
|
|
@@ -237,11 +293,11 @@ export class Client {
|
|
|
237
293
|
this.timeout = timeout;
|
|
238
294
|
this.batchSize = batchSize;
|
|
239
295
|
this.flushDelay = flushDelay;
|
|
240
|
-
this.maxQueueSize = maxQueueSize;
|
|
241
296
|
this.logLevel = logLevel;
|
|
242
297
|
|
|
243
|
-
|
|
244
|
-
this.
|
|
298
|
+
void maxQueueSize;
|
|
299
|
+
this.queueLock = new AsyncLock();
|
|
300
|
+
this.flushLock = new AsyncLock();
|
|
245
301
|
|
|
246
302
|
this.axiosInstance = axios.create({
|
|
247
303
|
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
@@ -280,11 +336,22 @@ export class Client {
|
|
|
280
336
|
return false;
|
|
281
337
|
}
|
|
282
338
|
|
|
339
|
+
if (log.log_id !== undefined) {
|
|
340
|
+
if (typeof log.log_id !== "string") {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const trimmedLogID = log.log_id.trim();
|
|
345
|
+
if (trimmedLogID.length > 0 && !isUUID(trimmedLogID)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
283
350
|
if (typeof log.value !== "number" || !Number.isFinite(log.value)) {
|
|
284
351
|
return false;
|
|
285
352
|
}
|
|
286
353
|
|
|
287
|
-
if (log.
|
|
354
|
+
if (log.agent !== undefined && typeof log.agent !== "string") {
|
|
288
355
|
return false;
|
|
289
356
|
}
|
|
290
357
|
|
|
@@ -319,14 +386,15 @@ export class Client {
|
|
|
319
386
|
|
|
320
387
|
private normalizeLog(log: Log): Log {
|
|
321
388
|
const normalizedLog: Log = {
|
|
389
|
+
log_id: resolveLogID(log.log_id),
|
|
322
390
|
event: log.event.trim(),
|
|
323
391
|
value: log.value,
|
|
324
392
|
};
|
|
325
393
|
|
|
326
|
-
if (typeof log.
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
329
|
-
normalizedLog.
|
|
394
|
+
if (typeof log.agent === "string") {
|
|
395
|
+
const trimmedAgent = log.agent.trim();
|
|
396
|
+
if (trimmedAgent.length > 0) {
|
|
397
|
+
normalizedLog.agent = trimmedAgent;
|
|
330
398
|
}
|
|
331
399
|
}
|
|
332
400
|
|
|
@@ -346,31 +414,17 @@ export class Client {
|
|
|
346
414
|
public async log(log: Log): Promise<void> {
|
|
347
415
|
if (!this.isValidLog(log)) {
|
|
348
416
|
throw new ChirpierError(
|
|
349
|
-
"Invalid log format: event must not be empty, value must be a finite number,
|
|
417
|
+
"Invalid log format: log_id must be a UUID when provided, event must not be empty, value must be a finite number, agent 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
418
|
"INVALID_LOG"
|
|
351
419
|
);
|
|
352
420
|
}
|
|
353
421
|
|
|
354
422
|
const normalizedLog = this.normalizeLog(log);
|
|
355
423
|
|
|
356
|
-
let queueFull = false;
|
|
357
|
-
|
|
358
424
|
await this.queueLock.acquire("queue", async () => {
|
|
359
|
-
|
|
360
|
-
queueFull = true;
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
this.logQueue.push({ log: normalizedLog, timestamp: Date.now(), retryCount: 0 });
|
|
425
|
+
this.logQueue.push({ log: normalizedLog, timestamp: Date.now() });
|
|
365
426
|
});
|
|
366
427
|
|
|
367
|
-
if (queueFull) {
|
|
368
|
-
throw new ChirpierError(
|
|
369
|
-
`Log queue is full (max size: ${this.maxQueueSize})`,
|
|
370
|
-
"QUEUE_FULL"
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
428
|
if (this.logQueue.length >= this.batchSize) {
|
|
375
429
|
await this.flushQueue();
|
|
376
430
|
} else if (!this.flushTimeoutId) {
|
|
@@ -412,24 +466,8 @@ export class Client {
|
|
|
412
466
|
console.error("Failed to send logs:", error);
|
|
413
467
|
}
|
|
414
468
|
|
|
415
|
-
const retryableLogs: QueuedLog[] = [];
|
|
416
|
-
for (const queuedLog of logsToSend) {
|
|
417
|
-
if (queuedLog.retryCount >= this.retries) {
|
|
418
|
-
if (this.logLevel >= LogLevel.Error) {
|
|
419
|
-
console.error(
|
|
420
|
-
`Dropping log after ${this.retries} retries:`,
|
|
421
|
-
queuedLog.log
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
queuedLog.retryCount++;
|
|
428
|
-
retryableLogs.push(queuedLog);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
469
|
await this.queueLock.acquire("logQueue", async () => {
|
|
432
|
-
this.logQueue = [...
|
|
470
|
+
this.logQueue = [...logsToSend, ...this.logQueue];
|
|
433
471
|
});
|
|
434
472
|
}
|
|
435
473
|
});
|
|
@@ -456,21 +494,26 @@ export class Client {
|
|
|
456
494
|
await this.shutdown();
|
|
457
495
|
}
|
|
458
496
|
|
|
459
|
-
public async listEvents(): Promise<
|
|
460
|
-
const response = await this.axiosInstance.get<
|
|
497
|
+
public async listEvents(): Promise<Event[]> {
|
|
498
|
+
const response = await this.axiosInstance.get<Event[]>(`${this.servicerEndpoint}/events`);
|
|
499
|
+
return response.data;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
public async createEvent(payload: CreateEventPayload): Promise<Event> {
|
|
503
|
+
const response = await this.axiosInstance.post<Event>(`${this.servicerEndpoint}/events`, payload);
|
|
461
504
|
return response.data;
|
|
462
505
|
}
|
|
463
506
|
|
|
464
|
-
public async getEvent(eventID: string): Promise<
|
|
465
|
-
const response = await this.axiosInstance.get<
|
|
507
|
+
public async getEvent(eventID: string): Promise<Event> {
|
|
508
|
+
const response = await this.axiosInstance.get<Event>(`${this.servicerEndpoint}/events/${eventID}`);
|
|
466
509
|
return response.data;
|
|
467
510
|
}
|
|
468
511
|
|
|
469
512
|
public async updateEvent(
|
|
470
513
|
eventID: string,
|
|
471
|
-
payload: Partial<Omit<
|
|
472
|
-
): Promise<
|
|
473
|
-
const response = await this.axiosInstance.put<
|
|
514
|
+
payload: Partial<Omit<Event, "event_id" | "created_at">>
|
|
515
|
+
): Promise<Event> {
|
|
516
|
+
const response = await this.axiosInstance.put<Event>(
|
|
474
517
|
`${this.servicerEndpoint}/events/${eventID}`,
|
|
475
518
|
payload
|
|
476
519
|
);
|
|
@@ -482,11 +525,21 @@ export class Client {
|
|
|
482
525
|
return response.data;
|
|
483
526
|
}
|
|
484
527
|
|
|
485
|
-
public async
|
|
528
|
+
public async getPolicy(policyID: string): Promise<Policy> {
|
|
529
|
+
const response = await this.axiosInstance.get<Policy>(`${this.servicerEndpoint}/policies/${policyID}`);
|
|
530
|
+
return response.data;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
public async createPolicy(payload: CreatePolicyPayload): Promise<Policy> {
|
|
486
534
|
const response = await this.axiosInstance.post<Policy>(`${this.servicerEndpoint}/policies`, payload);
|
|
487
535
|
return response.data;
|
|
488
536
|
}
|
|
489
537
|
|
|
538
|
+
public async updatePolicy(policyID: string, payload: UpdatePolicyPayload): Promise<Policy> {
|
|
539
|
+
const response = await this.axiosInstance.put<Policy>(`${this.servicerEndpoint}/policies/${policyID}`, payload);
|
|
540
|
+
return response.data;
|
|
541
|
+
}
|
|
542
|
+
|
|
490
543
|
public async listAlerts(status?: string): Promise<Alert[]> {
|
|
491
544
|
const endpoint = status
|
|
492
545
|
? `${this.servicerEndpoint}/alerts?status=${encodeURIComponent(status)}`
|
|
@@ -495,6 +548,11 @@ export class Client {
|
|
|
495
548
|
return response.data;
|
|
496
549
|
}
|
|
497
550
|
|
|
551
|
+
public async getAlert(alertID: string): Promise<Alert> {
|
|
552
|
+
const response = await this.axiosInstance.get<Alert>(`${this.servicerEndpoint}/alerts/${alertID}`);
|
|
553
|
+
return response.data;
|
|
554
|
+
}
|
|
555
|
+
|
|
498
556
|
public async getAlertDeliveries(alertID: string, options: { limit?: number; offset?: number; kind?: DeliveryKind } = {}): Promise<AlertDelivery[]> {
|
|
499
557
|
const params = new URLSearchParams();
|
|
500
558
|
if (options.kind) {
|
|
@@ -521,8 +579,29 @@ export class Client {
|
|
|
521
579
|
return response.data;
|
|
522
580
|
}
|
|
523
581
|
|
|
524
|
-
public async
|
|
525
|
-
await this.axiosInstance.
|
|
582
|
+
public async listDestinations(): Promise<Destination[]> {
|
|
583
|
+
const response = await this.axiosInstance.get<Destination[]>(`${this.servicerEndpoint}/destinations`);
|
|
584
|
+
return response.data;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
public async createDestination(payload: CreateDestinationPayload): Promise<Destination> {
|
|
588
|
+
const response = await this.axiosInstance.post<Destination>(`${this.servicerEndpoint}/destinations`, payload);
|
|
589
|
+
return response.data;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
public async getDestination(destinationID: string): Promise<Destination> {
|
|
593
|
+
const response = await this.axiosInstance.get<Destination>(`${this.servicerEndpoint}/destinations/${destinationID}`);
|
|
594
|
+
return response.data;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
public async updateDestination(destinationID: string, payload: UpdateDestinationPayload): Promise<Destination> {
|
|
598
|
+
const response = await this.axiosInstance.put<Destination>(`${this.servicerEndpoint}/destinations/${destinationID}`, payload);
|
|
599
|
+
return response.data;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
public async testDestination(destinationID: string): Promise<DestinationTestResult> {
|
|
603
|
+
const response = await this.axiosInstance.post<DestinationTestResult>(`${this.servicerEndpoint}/destinations/${destinationID}/test`);
|
|
604
|
+
return response.data;
|
|
526
605
|
}
|
|
527
606
|
|
|
528
607
|
public async getEventLogs(eventID: string, options: PaginationOptions = {}): Promise<EventLogPoint[]> {
|
|
@@ -541,6 +620,15 @@ export class Client {
|
|
|
541
620
|
return response.data;
|
|
542
621
|
}
|
|
543
622
|
|
|
623
|
+
public async getEventAnalytics(eventID: string, query: AnalyticsWindowQuery): Promise<AnalyticsWindowResponse> {
|
|
624
|
+
const params = new URLSearchParams();
|
|
625
|
+
params.set("view", query.view);
|
|
626
|
+
params.set("period", query.period);
|
|
627
|
+
params.set("previous", query.previous);
|
|
628
|
+
const response = await this.axiosInstance.get<AnalyticsWindowResponse>(`${this.servicerEndpoint}/events/${eventID}/analytics?${params.toString()}`);
|
|
629
|
+
return response.data;
|
|
630
|
+
}
|
|
631
|
+
|
|
544
632
|
public async resolveAlert(alertID: string): Promise<Alert> {
|
|
545
633
|
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/resolve`);
|
|
546
634
|
return response.data;
|
|
@@ -562,6 +650,21 @@ function isValidAPIKey(token: string): boolean {
|
|
|
562
650
|
return token.startsWith("chp_") && token.length > "chp_".length;
|
|
563
651
|
}
|
|
564
652
|
|
|
653
|
+
function isUUID(value: string): boolean {
|
|
654
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function resolveLogID(logID?: string): string {
|
|
658
|
+
if (typeof logID === "string") {
|
|
659
|
+
const trimmedLogID = logID.trim();
|
|
660
|
+
if (trimmedLogID.length > 0) {
|
|
661
|
+
return trimmedLogID;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return uuidv7();
|
|
666
|
+
}
|
|
667
|
+
|
|
565
668
|
function loadDotEnvKey(): string | undefined {
|
|
566
669
|
if (!isNodeEnvironment()) {
|
|
567
670
|
return undefined;
|