@pinelab/vendure-plugin-alerting 1.0.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/CHANGELOG.md +6 -0
- package/README.md +126 -0
- package/dist/alerting.plugin.d.ts +6 -0
- package/dist/alerting.plugin.js +43 -0
- package/dist/config/alert.d.ts +82 -0
- package/dist/config/alert.js +97 -0
- package/dist/config/notifier.d.ts +14 -0
- package/dist/config/notifier.js +2 -0
- package/dist/config/notifiers/email-notifier.d.ts +26 -0
- package/dist/config/notifiers/email-notifier.js +27 -0
- package/dist/config/notifiers/webhook-notifier.d.ts +15 -0
- package/dist/config/notifiers/webhook-notifier.js +32 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +23 -0
- package/dist/services/alert-logger.d.ts +20 -0
- package/dist/services/alert-logger.js +51 -0
- package/dist/services/alerting.service.d.ts +20 -0
- package/dist/services/alerting.service.js +150 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +2 -0
- package/package.json +30 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Vendure Alerting Plugin
|
|
2
|
+
|
|
3
|
+
[Official documentation here](https://plugins.pinelab.studio/plugin/vendure-plugin-alerting)
|
|
4
|
+
|
|
5
|
+
Send notifications based on Vendure events and log messages via configurable notifiers like webhooks, slack or N8N.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
AlertingPlugin,
|
|
12
|
+
EventAlert,
|
|
13
|
+
LogAlert,
|
|
14
|
+
WebhookNotifier,
|
|
15
|
+
} from '@pinelab/vendure-plugin-alerting';
|
|
16
|
+
import { PaymentStateTransitionEvent } from '@vendure/core';
|
|
17
|
+
|
|
18
|
+
// Slack example. Get the webhook URL from your slack account: https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
|
|
19
|
+
const slack = new WebhookNotifier({
|
|
20
|
+
name: 'slack',
|
|
21
|
+
url: process.env.SLACK_WEBHOOK!,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const n8n = new WebhookNotifier({
|
|
25
|
+
name: 'n8n',
|
|
26
|
+
url: 'https://n8n.example.com/webhook/alert',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const plugins: VendurePlugin[] = [
|
|
30
|
+
AlertingPlugin.init({
|
|
31
|
+
alerts: [
|
|
32
|
+
// Alert on order payment failure
|
|
33
|
+
new EventAlert([slack, n8n])
|
|
34
|
+
.on(PaymentStateTransitionEvent)
|
|
35
|
+
.filter((e) => e.toState === 'Error') // Use this to decide when to alert
|
|
36
|
+
.notify((e) => `Payment error for order "${e.order.code}"`), // Notifies via Slack and n8n
|
|
37
|
+
|
|
38
|
+
// Alert on any error log from PaymentService
|
|
39
|
+
new LogAlert([slack])
|
|
40
|
+
.onLog('error', 'warn')
|
|
41
|
+
.filter((log) => log.loggerCtx === 'PaymentService')
|
|
42
|
+
.notify((log) => ({
|
|
43
|
+
subject: `[${log.level}] ${log.loggerCtx}`,
|
|
44
|
+
text: log.message,
|
|
45
|
+
})),
|
|
46
|
+
],
|
|
47
|
+
// Deduplicate identical notifications to prevent alert storms: Only notify every once per X ms
|
|
48
|
+
deduplicationWindowMs: 60_000, // default
|
|
49
|
+
}),
|
|
50
|
+
];
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Email notifier
|
|
54
|
+
|
|
55
|
+
Send alerts as plain-text emails via Vendure's built-in `NodemailerEmailSender`. No templates are required — the alert subject and text are sent directly.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { EmailNotifier } from '@pinelab/vendure-plugin-alerting';
|
|
59
|
+
|
|
60
|
+
const email = new EmailNotifier({
|
|
61
|
+
name: 'email',
|
|
62
|
+
from: 'alerts@example.com',
|
|
63
|
+
to: 'admin@example.com',
|
|
64
|
+
transport: {
|
|
65
|
+
type: 'smtp',
|
|
66
|
+
host: 'smtp.example.com',
|
|
67
|
+
port: 587,
|
|
68
|
+
auth: {
|
|
69
|
+
user: 'username',
|
|
70
|
+
pass: 'password',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
new LogAlert([email]).onLog('error').notify((log) => ({
|
|
76
|
+
subject: `[${log.level}] ${log.loggerCtx}`,
|
|
77
|
+
text: log.message,
|
|
78
|
+
}));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You can use any transport supported by the EmailPlugin, such as `ses`, `sendmail`, or `file`. You can also pass a custom `EmailSender` via the `emailSender` option.
|
|
82
|
+
|
|
83
|
+
## Custom Notifiers
|
|
84
|
+
|
|
85
|
+
Implement the `Notifier` interface to create custom channels (e.g. SMS) and use custom metadata.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { AlertMessage, Notifier } from '@pinelab/vendure-plugin-alerting';
|
|
89
|
+
|
|
90
|
+
// Example notifier that uses custom metadata
|
|
91
|
+
export class CustomNotifier implements Notifier {
|
|
92
|
+
name = 'my-custom-notifier';
|
|
93
|
+
|
|
94
|
+
notify({subject, text, metadata}: AlertMessage): Promise<void> {
|
|
95
|
+
// This is where you would implement the logic to send the alert message to your desired destination.
|
|
96
|
+
// With the given metadata you could send custom body or headers for example.
|
|
97
|
+
const authorization = metadata.myHeaders.bearerToken
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// You would use this Notifier like this:
|
|
102
|
+
const customNotifier = new CustomNotifier();
|
|
103
|
+
new EventAlert([customNotifier])
|
|
104
|
+
.on(PaymentStateTransitionEvent)
|
|
105
|
+
.filter((e) => e.toState === 'Error')
|
|
106
|
+
.notify((log) => ({
|
|
107
|
+
subject: `My subject`,
|
|
108
|
+
text: `my custom message`,
|
|
109
|
+
metadata: {
|
|
110
|
+
key: "you can pass anything you want to your custom notifier here"
|
|
111
|
+
key2: "It is up to your notifier to use the metadata object"
|
|
112
|
+
}
|
|
113
|
+
})),
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Deduplication
|
|
117
|
+
|
|
118
|
+
Identical alerts (same notifier + subject + text) fired within `deduplicationWindowMs` are dropped. This prevents alert storms when many events fire in rapid succession.
|
|
119
|
+
|
|
120
|
+
## Retry
|
|
121
|
+
|
|
122
|
+
Alert delivery is backed by the Vendure JobQueue. If a notifier throws, the job is retried with the configured retry strategy.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var AlertingPlugin_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.AlertingPlugin = void 0;
|
|
11
|
+
const core_1 = require("@vendure/core");
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
const alerting_service_1 = require("./services/alerting.service");
|
|
14
|
+
let AlertingPlugin = AlertingPlugin_1 = class AlertingPlugin {
|
|
15
|
+
static init(options) {
|
|
16
|
+
this.options = {
|
|
17
|
+
...this.options,
|
|
18
|
+
...options,
|
|
19
|
+
};
|
|
20
|
+
return AlertingPlugin_1;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
exports.AlertingPlugin = AlertingPlugin;
|
|
24
|
+
AlertingPlugin.options = {
|
|
25
|
+
alerts: [],
|
|
26
|
+
};
|
|
27
|
+
exports.AlertingPlugin = AlertingPlugin = AlertingPlugin_1 = __decorate([
|
|
28
|
+
(0, core_1.VendurePlugin)({
|
|
29
|
+
imports: [core_1.PluginCommonModule],
|
|
30
|
+
providers: [
|
|
31
|
+
alerting_service_1.AlertingService,
|
|
32
|
+
{
|
|
33
|
+
provide: constants_1.PLUGIN_INIT_OPTIONS,
|
|
34
|
+
useFactory: () => AlertingPlugin.options,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
configuration: (config) => {
|
|
38
|
+
config.logger = alerting_service_1.AlertingService.wrapLogger(config.logger);
|
|
39
|
+
return config;
|
|
40
|
+
},
|
|
41
|
+
compatibility: '>=3.2.0',
|
|
42
|
+
})
|
|
43
|
+
], AlertingPlugin);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Type, VendureEvent } from '@vendure/core';
|
|
2
|
+
import { Notifier } from './notifier';
|
|
3
|
+
import { AlertMessage, AlertLogLevel, LogAlertContext } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base for alert definitions consumed by {@link AlertingService}.
|
|
6
|
+
* Holds the shared notifier list, optional filter, and message builder.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - The type of trigger this alert receives (a Vendure event or
|
|
9
|
+
* a {@link LogAlertContext}).
|
|
10
|
+
*/
|
|
11
|
+
export declare abstract class BaseAlert<T> {
|
|
12
|
+
readonly notifiers: Notifier[];
|
|
13
|
+
/** Optional predicate that decides whether the alert should fire. */
|
|
14
|
+
filterFn?: (trigger: T) => boolean | Promise<boolean>;
|
|
15
|
+
/** Function that converts the trigger into an {@link AlertMessage}
|
|
16
|
+
* (or a plain string, which is auto-wrapped as `{ subject: 'Alert', text: <string> }`). */
|
|
17
|
+
notifyFn: (trigger: T) => string | AlertMessage | Promise<string | AlertMessage>;
|
|
18
|
+
/**
|
|
19
|
+
* @param notifiers - The channels that will receive the alert when it fires.
|
|
20
|
+
*/
|
|
21
|
+
constructor(notifiers: Notifier[]);
|
|
22
|
+
/**
|
|
23
|
+
* Only fire the alert when the predicate returns true for the trigger.
|
|
24
|
+
*/
|
|
25
|
+
filter(fn: (trigger: T) => boolean | Promise<boolean>): this;
|
|
26
|
+
/**
|
|
27
|
+
* Define how the {@link AlertMessage} is built from the trigger.
|
|
28
|
+
* Returning a string is shorthand for `{ subject: 'Alert', text: <string> }`.
|
|
29
|
+
*/
|
|
30
|
+
notify(fn: (trigger: T) => string | AlertMessage | Promise<string | AlertMessage>): this;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builder for an alert rule that reacts to Vendure {@linkcode EventBus} events.
|
|
34
|
+
*
|
|
35
|
+
* The generic parameter {@linkcode E} is automatically inferred from the events
|
|
36
|
+
* passed to {@linkcode on()}. It starts as `never` so the alert only fires when
|
|
37
|
+
* explicitly subscribed to event types.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* new EventAlert([webhookNotifier])
|
|
42
|
+
* .on(ProductEvent, OrderPlacedEvent)
|
|
43
|
+
* .filter(e => e.type === 'created')
|
|
44
|
+
* .notify(e => `Product ${e.type}`);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class EventAlert<E extends VendureEvent = never> extends BaseAlert<E> {
|
|
48
|
+
/** Vendure event types this alert subscribes to. */
|
|
49
|
+
events: Type<VendureEvent>[];
|
|
50
|
+
/**
|
|
51
|
+
* Trigger this alert when any of the given Vendure events are published.
|
|
52
|
+
*
|
|
53
|
+
* @returns A new {@linkcode EventAlert} whose event union now includes all
|
|
54
|
+
* the events passed in.
|
|
55
|
+
*/
|
|
56
|
+
on<T extends VendureEvent[]>(...events: {
|
|
57
|
+
[I in keyof T]: Type<T[I]>;
|
|
58
|
+
}): EventAlert<E | T[number]>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Builder for an alert rule that reacts to log messages emitted through
|
|
62
|
+
* Vendure's {@linkcode VendureLogger}.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* new LogAlert([webhookNotifier])
|
|
67
|
+
* .onLog('error', 'warn')
|
|
68
|
+
* .filter(log => log.loggerCtx === 'PaymentGateway')
|
|
69
|
+
* .notify(log => ({
|
|
70
|
+
* subject: `[${log.level}] Payment issue`,
|
|
71
|
+
* text: log.message,
|
|
72
|
+
* }));
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare class LogAlert extends BaseAlert<LogAlertContext> {
|
|
76
|
+
/** Log levels this alert subscribes to. */
|
|
77
|
+
logLevels: AlertLogLevel[];
|
|
78
|
+
/**
|
|
79
|
+
* Trigger this alert when a log message is emitted at any of the given levels.
|
|
80
|
+
*/
|
|
81
|
+
onLog(...levels: AlertLogLevel[]): this;
|
|
82
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogAlert = exports.EventAlert = exports.BaseAlert = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base for alert definitions consumed by {@link AlertingService}.
|
|
6
|
+
* Holds the shared notifier list, optional filter, and message builder.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - The type of trigger this alert receives (a Vendure event or
|
|
9
|
+
* a {@link LogAlertContext}).
|
|
10
|
+
*/
|
|
11
|
+
class BaseAlert {
|
|
12
|
+
/**
|
|
13
|
+
* @param notifiers - The channels that will receive the alert when it fires.
|
|
14
|
+
*/
|
|
15
|
+
constructor(notifiers) {
|
|
16
|
+
this.notifiers = notifiers;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Only fire the alert when the predicate returns true for the trigger.
|
|
20
|
+
*/
|
|
21
|
+
filter(fn) {
|
|
22
|
+
this.filterFn = fn;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Define how the {@link AlertMessage} is built from the trigger.
|
|
27
|
+
* Returning a string is shorthand for `{ subject: 'Alert', text: <string> }`.
|
|
28
|
+
*/
|
|
29
|
+
notify(fn) {
|
|
30
|
+
this.notifyFn = fn;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.BaseAlert = BaseAlert;
|
|
35
|
+
/**
|
|
36
|
+
* Builder for an alert rule that reacts to Vendure {@linkcode EventBus} events.
|
|
37
|
+
*
|
|
38
|
+
* The generic parameter {@linkcode E} is automatically inferred from the events
|
|
39
|
+
* passed to {@linkcode on()}. It starts as `never` so the alert only fires when
|
|
40
|
+
* explicitly subscribed to event types.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* new EventAlert([webhookNotifier])
|
|
45
|
+
* .on(ProductEvent, OrderPlacedEvent)
|
|
46
|
+
* .filter(e => e.type === 'created')
|
|
47
|
+
* .notify(e => `Product ${e.type}`);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class EventAlert extends BaseAlert {
|
|
51
|
+
constructor() {
|
|
52
|
+
super(...arguments);
|
|
53
|
+
/** Vendure event types this alert subscribes to. */
|
|
54
|
+
this.events = [];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Trigger this alert when any of the given Vendure events are published.
|
|
58
|
+
*
|
|
59
|
+
* @returns A new {@linkcode EventAlert} whose event union now includes all
|
|
60
|
+
* the events passed in.
|
|
61
|
+
*/
|
|
62
|
+
on(...events) {
|
|
63
|
+
this.events.push(...events);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.EventAlert = EventAlert;
|
|
68
|
+
/**
|
|
69
|
+
* Builder for an alert rule that reacts to log messages emitted through
|
|
70
|
+
* Vendure's {@linkcode VendureLogger}.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* new LogAlert([webhookNotifier])
|
|
75
|
+
* .onLog('error', 'warn')
|
|
76
|
+
* .filter(log => log.loggerCtx === 'PaymentGateway')
|
|
77
|
+
* .notify(log => ({
|
|
78
|
+
* subject: `[${log.level}] Payment issue`,
|
|
79
|
+
* text: log.message,
|
|
80
|
+
* }));
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
class LogAlert extends BaseAlert {
|
|
84
|
+
constructor() {
|
|
85
|
+
super(...arguments);
|
|
86
|
+
/** Log levels this alert subscribes to. */
|
|
87
|
+
this.logLevels = [];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Trigger this alert when a log message is emitted at any of the given levels.
|
|
91
|
+
*/
|
|
92
|
+
onLog(...levels) {
|
|
93
|
+
this.logLevels.push(...levels);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.LogAlert = LogAlert;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AlertMessage } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Defines an outgoing notification channel (e.g. webhook, email, SMS).
|
|
4
|
+
* Each notifier has a unique name and knows how to deliver an {@link AlertMessage}.
|
|
5
|
+
*/
|
|
6
|
+
export interface Notifier {
|
|
7
|
+
/** Unique name; used as job-data discriminator and in logs */
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/**
|
|
10
|
+
* Deliver the alert message.
|
|
11
|
+
* Throwing causes the JobQueue to retry the delivery.
|
|
12
|
+
*/
|
|
13
|
+
notify(message: AlertMessage): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AlertMessage } from '../../types';
|
|
2
|
+
import { Notifier } from '../notifier';
|
|
3
|
+
import { EmailSender, EmailTransportOptions } from '@vendure/email-plugin';
|
|
4
|
+
export interface EmailNotifierConfig {
|
|
5
|
+
/** Unique name; used as job-data discriminator and in logs */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Sender address */
|
|
8
|
+
from: string;
|
|
9
|
+
/** Recipient address */
|
|
10
|
+
to: string;
|
|
11
|
+
/** Vendure email transport options (e.g. SMTP, SES, file) */
|
|
12
|
+
transport: EmailTransportOptions;
|
|
13
|
+
/** Optional custom EmailSender; defaults to NodemailerEmailSender */
|
|
14
|
+
emailSender?: EmailSender;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Notifier that sends alert messages as plain-text emails via Vendure's
|
|
18
|
+
* built-in {@link NodemailerEmailSender}. No templates are required —
|
|
19
|
+
* the alert subject and text are sent directly as the email subject and body.
|
|
20
|
+
*/
|
|
21
|
+
export declare class EmailNotifier implements Notifier {
|
|
22
|
+
private config;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
constructor(config: EmailNotifierConfig);
|
|
25
|
+
notify(message: AlertMessage): Promise<void>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EmailNotifier = void 0;
|
|
4
|
+
const email_plugin_1 = require("@vendure/email-plugin");
|
|
5
|
+
/**
|
|
6
|
+
* Notifier that sends alert messages as plain-text emails via Vendure's
|
|
7
|
+
* built-in {@link NodemailerEmailSender}. No templates are required —
|
|
8
|
+
* the alert subject and text are sent directly as the email subject and body.
|
|
9
|
+
*/
|
|
10
|
+
class EmailNotifier {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.name = config.name;
|
|
14
|
+
}
|
|
15
|
+
async notify(message) {
|
|
16
|
+
const emailDetails = {
|
|
17
|
+
from: this.config.from,
|
|
18
|
+
recipient: this.config.to,
|
|
19
|
+
subject: message.subject,
|
|
20
|
+
body: message.text,
|
|
21
|
+
attachments: [],
|
|
22
|
+
};
|
|
23
|
+
const sender = this.config.emailSender ?? new email_plugin_1.NodemailerEmailSender();
|
|
24
|
+
await sender.send(emailDetails, this.config.transport);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.EmailNotifier = EmailNotifier;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AlertMessage } from '../../types';
|
|
2
|
+
import { Notifier } from '../notifier';
|
|
3
|
+
interface WebhookNotifierConfig {
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
method?: 'POST' | 'GET';
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare class WebhookNotifier implements Notifier {
|
|
10
|
+
private config;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
constructor(config: WebhookNotifierConfig);
|
|
13
|
+
notify(message: AlertMessage): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WebhookNotifier = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
class WebhookNotifier {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.name = config.name;
|
|
12
|
+
}
|
|
13
|
+
async notify(message) {
|
|
14
|
+
const headers = {
|
|
15
|
+
'content-type': 'application/json',
|
|
16
|
+
...(this.config.headers || {}),
|
|
17
|
+
...(message.metadata?.headers || {}),
|
|
18
|
+
};
|
|
19
|
+
const body = this.config.method === 'GET'
|
|
20
|
+
? undefined
|
|
21
|
+
: JSON.stringify(message.metadata?.body ?? message);
|
|
22
|
+
const res = await (0, node_fetch_1.default)(this.config.url, {
|
|
23
|
+
method: this.config.method || 'POST',
|
|
24
|
+
headers,
|
|
25
|
+
body,
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`Webhook ${this.name} returned ${res.status}: ${await res.text()}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.WebhookNotifier = WebhookNotifier;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './alerting.plugin';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './config/alert';
|
|
4
|
+
export * from './config/notifier';
|
|
5
|
+
export * from './config/notifiers/webhook-notifier';
|
|
6
|
+
export * from './config/notifiers/email-notifier';
|
|
7
|
+
export * from './services/alerting.service';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./alerting.plugin"), exports);
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./config/alert"), exports);
|
|
20
|
+
__exportStar(require("./config/notifier"), exports);
|
|
21
|
+
__exportStar(require("./config/notifiers/webhook-notifier"), exports);
|
|
22
|
+
__exportStar(require("./config/notifiers/email-notifier"), exports);
|
|
23
|
+
__exportStar(require("./services/alerting.service"), exports);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { VendureLogger } from '@vendure/core';
|
|
2
|
+
import { AlertLogLevel, LogAlertContext } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* VendureLogger wrapper that forwards log calls to the original logger
|
|
5
|
+
* and buffers entries so the AlertingService can process them.
|
|
6
|
+
*/
|
|
7
|
+
export declare class AlertingLogger implements VendureLogger {
|
|
8
|
+
private readonly wrapped;
|
|
9
|
+
private static logBuffer;
|
|
10
|
+
private static isReady;
|
|
11
|
+
private static onLogHandler?;
|
|
12
|
+
static setHandler(handler: (log: LogAlertContext) => void): void;
|
|
13
|
+
static pushLog(level: AlertLogLevel, message: string, ctx?: string): void;
|
|
14
|
+
constructor(wrapped: VendureLogger);
|
|
15
|
+
info(message: string, ctx?: string): void;
|
|
16
|
+
warn(message: string, ctx?: string): void;
|
|
17
|
+
error(message: string, ctx?: string, trace?: string): void;
|
|
18
|
+
debug(message: string, ctx?: string): void;
|
|
19
|
+
verbose(message: string, ctx?: string): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AlertingLogger = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* VendureLogger wrapper that forwards log calls to the original logger
|
|
6
|
+
* and buffers entries so the AlertingService can process them.
|
|
7
|
+
*/
|
|
8
|
+
class AlertingLogger {
|
|
9
|
+
static setHandler(handler) {
|
|
10
|
+
this.onLogHandler = handler;
|
|
11
|
+
this.isReady = true;
|
|
12
|
+
// Flush buffered logs
|
|
13
|
+
while (this.logBuffer.length > 0) {
|
|
14
|
+
const log = this.logBuffer.shift();
|
|
15
|
+
handler(log);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
static pushLog(level, message, ctx) {
|
|
19
|
+
const log = { level, message, loggerCtx: ctx };
|
|
20
|
+
if (this.isReady && this.onLogHandler) {
|
|
21
|
+
this.onLogHandler(log);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.logBuffer.push(log);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
constructor(wrapped) {
|
|
28
|
+
this.wrapped = wrapped;
|
|
29
|
+
}
|
|
30
|
+
info(message, ctx) {
|
|
31
|
+
this.wrapped.info(message, ctx);
|
|
32
|
+
AlertingLogger.pushLog('info', message, ctx);
|
|
33
|
+
}
|
|
34
|
+
warn(message, ctx) {
|
|
35
|
+
this.wrapped.warn(message, ctx);
|
|
36
|
+
AlertingLogger.pushLog('warn', message, ctx);
|
|
37
|
+
}
|
|
38
|
+
error(message, ctx, trace) {
|
|
39
|
+
this.wrapped.error(message, ctx, trace);
|
|
40
|
+
AlertingLogger.pushLog('error', message, ctx);
|
|
41
|
+
}
|
|
42
|
+
debug(message, ctx) {
|
|
43
|
+
this.wrapped.debug(message, ctx);
|
|
44
|
+
}
|
|
45
|
+
verbose(message, ctx) {
|
|
46
|
+
this.wrapped.verbose(message, ctx);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.AlertingLogger = AlertingLogger;
|
|
50
|
+
AlertingLogger.logBuffer = [];
|
|
51
|
+
AlertingLogger.isReady = false;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { EventBus, JobQueueService, VendureLogger } from '@vendure/core';
|
|
3
|
+
import { AlertingPluginOptions } from '../types';
|
|
4
|
+
export declare class AlertingService implements OnModuleInit, OnApplicationBootstrap {
|
|
5
|
+
private eventBus;
|
|
6
|
+
private jobQueueService;
|
|
7
|
+
private options;
|
|
8
|
+
private jobQueue;
|
|
9
|
+
private dedupMap;
|
|
10
|
+
private notifiersByName;
|
|
11
|
+
constructor(eventBus: EventBus, jobQueueService: JobQueueService, options: AlertingPluginOptions);
|
|
12
|
+
onModuleInit(): Promise<void>;
|
|
13
|
+
onApplicationBootstrap(): void;
|
|
14
|
+
static wrapLogger(logger: VendureLogger): VendureLogger;
|
|
15
|
+
private handleTrigger;
|
|
16
|
+
private processJob;
|
|
17
|
+
private dedupKey;
|
|
18
|
+
private hash;
|
|
19
|
+
private cleanupDedupMap;
|
|
20
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AlertingService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@vendure/core");
|
|
18
|
+
const constants_1 = require("../constants");
|
|
19
|
+
const alert_1 = require("../config/alert");
|
|
20
|
+
const alert_logger_1 = require("./alert-logger");
|
|
21
|
+
let AlertingService = class AlertingService {
|
|
22
|
+
constructor(eventBus, jobQueueService, options) {
|
|
23
|
+
this.eventBus = eventBus;
|
|
24
|
+
this.jobQueueService = jobQueueService;
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.dedupMap = new Map();
|
|
27
|
+
this.notifiersByName = new Map();
|
|
28
|
+
}
|
|
29
|
+
async onModuleInit() {
|
|
30
|
+
this.jobQueue = await this.jobQueueService.createQueue({
|
|
31
|
+
name: 'alerting-notify',
|
|
32
|
+
process: async (job) => {
|
|
33
|
+
try {
|
|
34
|
+
await this.processJob(job.data.notifierName, job.data.alertMessage);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
38
|
+
core_1.Logger.error(`Failed to process alert job for notifier "${job.data.notifierName}": ${err.message}`, constants_1.loggerCtx, err.stack);
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
onApplicationBootstrap() {
|
|
45
|
+
// Build notifier lookup from all alerts
|
|
46
|
+
for (const alert of this.options.alerts) {
|
|
47
|
+
for (const notifier of alert.notifiers) {
|
|
48
|
+
this.notifiersByName.set(notifier.name, notifier);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Subscribe to configured events (EventAlert only)
|
|
52
|
+
for (const alert of this.options.alerts) {
|
|
53
|
+
if (alert instanceof alert_1.EventAlert) {
|
|
54
|
+
for (const EventType of alert.events) {
|
|
55
|
+
this.eventBus.ofType(EventType).subscribe(async (event) => {
|
|
56
|
+
try {
|
|
57
|
+
await this.handleTrigger(alert, event);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
core_1.Logger.error(`Failed to handle event ${event.constructor.name} for alert: ${e}`, constants_1.loggerCtx);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Subscribe to log buffer (LogAlert only)
|
|
67
|
+
alert_logger_1.AlertingLogger.setHandler((log) => {
|
|
68
|
+
for (const alert of this.options.alerts) {
|
|
69
|
+
if (alert instanceof alert_1.LogAlert && alert.logLevels.includes(log.level)) {
|
|
70
|
+
this.handleTrigger(alert, log).catch((e) => {
|
|
71
|
+
core_1.Logger.error(`Failed to handle log alert: ${e}`, constants_1.loggerCtx);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
static wrapLogger(logger) {
|
|
78
|
+
return new alert_logger_1.AlertingLogger(logger);
|
|
79
|
+
}
|
|
80
|
+
async handleTrigger(alert, trigger) {
|
|
81
|
+
// Prevent infinite loops: skip logs emitted by this plugin
|
|
82
|
+
const logCtx = trigger.loggerCtx;
|
|
83
|
+
if (logCtx === constants_1.loggerCtx) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Apply filter if configured
|
|
87
|
+
if (alert.filterFn) {
|
|
88
|
+
const passes = await alert.filterFn(trigger);
|
|
89
|
+
if (!passes) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Build message
|
|
94
|
+
const raw = await alert.notifyFn(trigger);
|
|
95
|
+
const message = typeof raw === 'string' ? { subject: 'Alert', text: raw } : raw;
|
|
96
|
+
// Enqueue one job per notifier
|
|
97
|
+
for (const notifier of alert.notifiers) {
|
|
98
|
+
await this.jobQueue.add({
|
|
99
|
+
notifierName: notifier.name,
|
|
100
|
+
alertMessage: message,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async processJob(notifierName, message) {
|
|
105
|
+
const windowMs = this.options.deduplicationWindowMs ?? 60_000;
|
|
106
|
+
const key = this.dedupKey(notifierName, message);
|
|
107
|
+
const lastSent = this.dedupMap.get(key);
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
if (lastSent && now - lastSent < windowMs) {
|
|
110
|
+
core_1.Logger.info(`Dropping duplicate alert for notifier "${notifierName}" with subject "${message.subject}"`, constants_1.loggerCtx);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.dedupMap.set(key, now);
|
|
114
|
+
this.cleanupDedupMap(windowMs);
|
|
115
|
+
const notifier = this.notifiersByName.get(notifierName);
|
|
116
|
+
if (!notifier) {
|
|
117
|
+
throw new Error(`Notifier "${notifierName}" not found`);
|
|
118
|
+
}
|
|
119
|
+
await notifier.notify(message);
|
|
120
|
+
core_1.Logger.info(`Sent alert to notifier "${notifierName}" with subject "${message.subject}"`, constants_1.loggerCtx);
|
|
121
|
+
}
|
|
122
|
+
dedupKey(notifierName, message) {
|
|
123
|
+
const str = `${notifierName}:${message.subject}:${message.text}`;
|
|
124
|
+
return this.hash(str);
|
|
125
|
+
}
|
|
126
|
+
hash(input) {
|
|
127
|
+
let hash = 0;
|
|
128
|
+
for (let i = 0; i < input.length; i++) {
|
|
129
|
+
const char = input.charCodeAt(i);
|
|
130
|
+
hash = (hash << 5) - hash + char;
|
|
131
|
+
hash |= 0;
|
|
132
|
+
}
|
|
133
|
+
return hash.toString(16);
|
|
134
|
+
}
|
|
135
|
+
cleanupDedupMap(windowMs) {
|
|
136
|
+
const cutoff = Date.now() - windowMs;
|
|
137
|
+
for (const [key, ts] of this.dedupMap.entries()) {
|
|
138
|
+
if (ts < cutoff) {
|
|
139
|
+
this.dedupMap.delete(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
exports.AlertingService = AlertingService;
|
|
145
|
+
exports.AlertingService = AlertingService = __decorate([
|
|
146
|
+
(0, common_1.Injectable)(),
|
|
147
|
+
__param(2, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
|
|
148
|
+
__metadata("design:paramtypes", [core_1.EventBus,
|
|
149
|
+
core_1.JobQueueService, Object])
|
|
150
|
+
], AlertingService);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventAlert, LogAlert } from './config/alert';
|
|
2
|
+
export interface AlertMessage {
|
|
3
|
+
subject: string;
|
|
4
|
+
text: string;
|
|
5
|
+
metadata?: Record<string, any>;
|
|
6
|
+
}
|
|
7
|
+
export type AlertLogLevel = 'info' | 'warn' | 'error';
|
|
8
|
+
export interface LogAlertContext {
|
|
9
|
+
level: AlertLogLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
loggerCtx?: string;
|
|
12
|
+
}
|
|
13
|
+
export type AlertTrigger<E = unknown> = E | LogAlertContext;
|
|
14
|
+
export interface AlertingPluginOptions {
|
|
15
|
+
alerts: Array<EventAlert<any> | LogAlert>;
|
|
16
|
+
/**
|
|
17
|
+
* Time window for deduplication in ms. Identical AlertMessages
|
|
18
|
+
* (same notifier + subject + text hash) within this window are dropped.
|
|
19
|
+
* Default: 60_000
|
|
20
|
+
*/
|
|
21
|
+
deduplicationWindowMs?: number;
|
|
22
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pinelab/vendure-plugin-alerting",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vendure plugin for sending alerts based on events and log to various notifiers like Slack, email or N8N",
|
|
5
|
+
"author": "Martijn van de Brug <martijn@pinelab.studio>",
|
|
6
|
+
"homepage": "https://plugins.pinelab.studio/",
|
|
7
|
+
"repository": "https://github.com/Pinelab-studio/pinelab-vendure-plugins",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"private": false,
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rimraf dist && tsc",
|
|
22
|
+
"start": "yarn ts-node test/dev-server.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"lint": "echo 'No linting configured'"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"node-fetch": "^2.6.7",
|
|
28
|
+
"catch-unknown": "^2.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|