@bsb/observable-graylog 9.1.2 → 9.6.12

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 CHANGED
@@ -7,7 +7,7 @@ Graylog (GELF) observable plugin for BSB that sends logs to Graylog servers usin
7
7
  - GELF 1.1 protocol support
8
8
  - Multiple transports: UDP, TCP, and HTTP
9
9
  - Custom fields with automatic trace and span integration
10
- - Optional gzip compression
10
+ - Optional gzip compression for UDP transport
11
11
  - Level filtering to control which logs are sent
12
12
 
13
13
  ## Installation
@@ -51,7 +51,7 @@ plugins:
51
51
  | `httpEndpoint` | HTTP endpoint URL (for HTTP protocol) | - |
52
52
  | `facility` | Facility name for log categorization | `bsb` |
53
53
  | `additionalFields` | Custom fields included in all messages | `{}` |
54
- | `compress` | Enable gzip compression | `true` |
54
+ | `compress` | Enable gzip compression for UDP transport | `true` |
55
55
  | `levels` | Log level filtering | All enabled |
56
56
 
57
57
  ## Usage
@@ -26,39 +26,39 @@
26
26
  */
27
27
  import { BSBObservable, BSBObservableConstructor, BSBError } from "@bsb/base";
28
28
  import { DTrace, LogMeta } from "@bsb/base";
29
- import * as av from "@anyvali/js";
29
+ import * as av from "anyvali";
30
30
  /**
31
31
  * Configuration schema for Graylog observable
32
32
  */
33
33
  export declare const GraylogConfigSchema: av.ObjectSchema<{
34
- host: av.OptionalSchema<av.StringSchema>;
35
- port: av.OptionalSchema<av.Int32Schema>;
36
- protocol: av.OptionalSchema<av.EnumSchema<string[]>>;
34
+ host: av.StringSchema;
35
+ port: av.Int32Schema;
36
+ protocol: av.EnumSchema<string[]>;
37
37
  httpEndpoint: av.OptionalSchema<av.StringSchema>;
38
- facility: av.OptionalSchema<av.StringSchema>;
38
+ facility: av.StringSchema;
39
39
  additionalFields: av.OptionalSchema<av.RecordSchema<av.UnionSchema<[av.StringSchema, av.NumberSchema, av.BoolSchema]>>>;
40
- compress: av.OptionalSchema<av.BoolSchema>;
40
+ compress: av.BoolSchema;
41
41
  levels: av.ObjectSchema<{
42
- debug: av.OptionalSchema<av.BoolSchema>;
43
- info: av.OptionalSchema<av.BoolSchema>;
44
- warn: av.OptionalSchema<av.BoolSchema>;
45
- error: av.OptionalSchema<av.BoolSchema>;
42
+ debug: av.BoolSchema;
43
+ info: av.BoolSchema;
44
+ warn: av.BoolSchema;
45
+ error: av.BoolSchema;
46
46
  }>;
47
47
  }>;
48
48
  export type GraylogConfig = av.Infer<typeof GraylogConfigSchema>;
49
49
  export declare const Config: import("@bsb/base").BSBPluginConfigClass<av.ObjectSchema<{
50
- host: av.OptionalSchema<av.StringSchema>;
51
- port: av.OptionalSchema<av.Int32Schema>;
52
- protocol: av.OptionalSchema<av.EnumSchema<string[]>>;
50
+ host: av.StringSchema;
51
+ port: av.Int32Schema;
52
+ protocol: av.EnumSchema<string[]>;
53
53
  httpEndpoint: av.OptionalSchema<av.StringSchema>;
54
- facility: av.OptionalSchema<av.StringSchema>;
54
+ facility: av.StringSchema;
55
55
  additionalFields: av.OptionalSchema<av.RecordSchema<av.UnionSchema<[av.StringSchema, av.NumberSchema, av.BoolSchema]>>>;
56
- compress: av.OptionalSchema<av.BoolSchema>;
56
+ compress: av.BoolSchema;
57
57
  levels: av.ObjectSchema<{
58
- debug: av.OptionalSchema<av.BoolSchema>;
59
- info: av.OptionalSchema<av.BoolSchema>;
60
- warn: av.OptionalSchema<av.BoolSchema>;
61
- error: av.OptionalSchema<av.BoolSchema>;
58
+ debug: av.BoolSchema;
59
+ info: av.BoolSchema;
60
+ warn: av.BoolSchema;
61
+ error: av.BoolSchema;
62
62
  }>;
63
63
  }>>;
64
64
  /**
@@ -66,22 +66,23 @@ export declare const Config: import("@bsb/base").BSBPluginConfigClass<av.ObjectS
66
66
  */
67
67
  export declare class Plugin extends BSBObservable<InstanceType<typeof Config>> {
68
68
  static Config: import("@bsb/base").BSBPluginConfigClass<av.ObjectSchema<{
69
- host: av.OptionalSchema<av.StringSchema>;
70
- port: av.OptionalSchema<av.Int32Schema>;
71
- protocol: av.OptionalSchema<av.EnumSchema<string[]>>;
69
+ host: av.StringSchema;
70
+ port: av.Int32Schema;
71
+ protocol: av.EnumSchema<string[]>;
72
72
  httpEndpoint: av.OptionalSchema<av.StringSchema>;
73
- facility: av.OptionalSchema<av.StringSchema>;
73
+ facility: av.StringSchema;
74
74
  additionalFields: av.OptionalSchema<av.RecordSchema<av.UnionSchema<[av.StringSchema, av.NumberSchema, av.BoolSchema]>>>;
75
- compress: av.OptionalSchema<av.BoolSchema>;
75
+ compress: av.BoolSchema;
76
76
  levels: av.ObjectSchema<{
77
- debug: av.OptionalSchema<av.BoolSchema>;
78
- info: av.OptionalSchema<av.BoolSchema>;
79
- warn: av.OptionalSchema<av.BoolSchema>;
80
- error: av.OptionalSchema<av.BoolSchema>;
77
+ debug: av.BoolSchema;
78
+ info: av.BoolSchema;
79
+ warn: av.BoolSchema;
80
+ error: av.BoolSchema;
81
81
  }>;
82
82
  }>>;
83
83
  private logFormatter;
84
84
  private isDisposed;
85
+ private defaultFields;
85
86
  constructor(config: BSBObservableConstructor<InstanceType<typeof Config>>);
86
87
  init(): Promise<void>;
87
88
  run(): Promise<void>;
@@ -89,6 +90,10 @@ export declare class Plugin extends BSBObservable<InstanceType<typeof Config>> {
89
90
  * Send log to Graylog
90
91
  */
91
92
  private sendLog;
93
+ private sendGelfMessage;
94
+ private sendUdp;
95
+ private sendTcp;
96
+ private sendHttp;
92
97
  debug(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
93
98
  info(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
94
99
  warn(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
@@ -25,34 +25,38 @@
25
25
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
26
26
  */
27
27
  import { BSBObservable, createConfigSchema, LogFormatter, BSBError } from "@bsb/base";
28
- import * as av from "@anyvali/js";
29
- import * as gelfPro from "gelf-pro";
28
+ import * as av from "anyvali";
29
+ import dgram from "node:dgram";
30
+ import net from "node:net";
31
+ import http from "node:http";
32
+ import https from "node:https";
33
+ import os from "node:os";
34
+ import { gzipSync } from "node:zlib";
30
35
  /**
31
36
  * Configuration schema for Graylog observable
32
37
  */
33
38
  export const GraylogConfigSchema = av.object({
34
- host: av.optional(av.string()).default("localhost"),
35
- port: av.optional(av.int32().min(1).max(65535)).default(12201),
36
- protocol: av.optional(av.enum_(["udp", "tcp", "http"])).default("udp"),
37
- httpEndpoint: av.optional(av.string().format("url")),
38
- facility: av.optional(av.string()).default("bsb"),
39
+ host: av.string().default("localhost").describe("Graylog server hostname"),
40
+ port: av.int32().min(1).max(65535).default(12201).describe("Graylog GELF server port"),
41
+ protocol: av.enum_(["udp", "tcp", "http"]).default("udp").describe("Transport protocol used to send GELF messages"),
42
+ httpEndpoint: av.optional(av.string().format("url")).describe("HTTP GELF endpoint used when protocol is http"),
43
+ facility: av.string().default("bsb").describe("GELF facility field value"),
39
44
  additionalFields: av.optional(av.record(av.union([
40
- av.string(),
41
- av.number(),
42
- av.bool(),
43
- ]))).default({}),
44
- compress: av.optional(av.bool()).default(true),
45
+ av.string().describe("String additional field value"),
46
+ av.number().describe("Numeric additional field value"),
47
+ av.bool().describe("Boolean additional field value"),
48
+ ]))).default({}).describe("Additional static GELF fields added to every log message"),
49
+ compress: av.bool().default(true).describe("Whether UDP GELF message compression is enabled"),
45
50
  levels: av.object({
46
- debug: av.optional(av.bool()).default(true),
47
- info: av.optional(av.bool()).default(true),
48
- warn: av.optional(av.bool()).default(true),
49
- error: av.optional(av.bool()).default(true),
50
- }, { unknownKeys: "strip" }),
51
- }, { unknownKeys: "strip" });
51
+ debug: av.bool().default(true).describe("Whether debug logs are sent to Graylog"),
52
+ info: av.bool().default(true).describe("Whether info logs are sent to Graylog"),
53
+ warn: av.bool().default(true).describe("Whether warning logs are sent to Graylog"),
54
+ error: av.bool().default(true).describe("Whether error logs are sent to Graylog"),
55
+ }).describe("Log level enablement"),
56
+ }).describe("Graylog observable plugin configuration");
52
57
  export const Config = createConfigSchema({
53
58
  name: 'observable-graylog',
54
59
  description: 'Graylog GELF observable plugin for centralized log ingestion',
55
- version: '9.0.0',
56
60
  image: './observable-graylog.png',
57
61
  tags: ['graylog', 'gelf', 'observability', 'logging'],
58
62
  documentation: ['./docs/plugin.md'],
@@ -83,27 +87,15 @@ export class Plugin extends BSBObservable {
83
87
  static Config = Config;
84
88
  logFormatter = new LogFormatter();
85
89
  isDisposed = false;
90
+ defaultFields = {};
86
91
  constructor(config) {
87
92
  super(config);
88
93
  }
89
94
  async init() {
90
- // Configure gelf-pro adapter
91
- const adapterName = this.config.protocol === "tcp" ? "tcp" : "udp";
92
- const adapterOptions = {
93
- host: this.config.host,
94
- port: this.config.port,
95
- };
96
- // Set default fields including facility
97
- const fields = {
95
+ this.defaultFields = {
98
96
  facility: this.config.facility,
99
97
  ...this.config.additionalFields,
100
98
  };
101
- // Initialize gelf-pro
102
- gelfPro.setConfig({
103
- adapterName,
104
- adapterOptions,
105
- fields,
106
- });
107
99
  }
108
100
  async run() {
109
101
  // No runtime setup needed
@@ -136,17 +128,68 @@ export class Plugin extends BSBObservable {
136
128
  }
137
129
  }
138
130
  }
139
- // Send to Graylog using the message method
140
- gelfPro.message(formattedMessage, gelfLevel, extraFields, (err) => {
141
- if (err) {
142
- console.error(`[observable-graylog] Failed to send log: ${err.message}`);
143
- }
144
- });
131
+ this.sendGelfMessage(formattedMessage, gelfLevel, extraFields);
145
132
  }
146
133
  catch (err) {
147
134
  console.error(`[observable-graylog] Send error: ${err.message}`);
148
135
  }
149
136
  }
137
+ sendGelfMessage(shortMessage, level, extraFields) {
138
+ const payload = JSON.stringify({
139
+ version: "1.1",
140
+ host: os.hostname(),
141
+ short_message: shortMessage,
142
+ timestamp: Date.now() / 1000,
143
+ level,
144
+ ...this.defaultFields,
145
+ ...extraFields,
146
+ });
147
+ if (this.config.protocol === "http") {
148
+ this.sendHttp(payload);
149
+ return;
150
+ }
151
+ if (this.config.protocol === "tcp") {
152
+ this.sendTcp(payload);
153
+ return;
154
+ }
155
+ this.sendUdp(payload);
156
+ }
157
+ sendUdp(payload) {
158
+ const socket = dgram.createSocket("udp4");
159
+ const message = this.config.compress ? gzipSync(Buffer.from(payload)) : Buffer.from(payload);
160
+ socket.send(message, this.config.port, this.config.host, (err) => {
161
+ socket.close();
162
+ if (err) {
163
+ console.error(`[observable-graylog] Failed to send UDP log: ${err.message}`);
164
+ }
165
+ });
166
+ }
167
+ sendTcp(payload) {
168
+ const socket = net.createConnection({ host: this.config.host, port: this.config.port }, () => {
169
+ socket.end(`${payload}\0`);
170
+ });
171
+ socket.on("error", (err) => {
172
+ console.error(`[observable-graylog] Failed to send TCP log: ${err.message}`);
173
+ });
174
+ }
175
+ sendHttp(payload) {
176
+ const endpoint = new URL(this.config.httpEndpoint ?? `http://${this.config.host}:${this.config.port}/gelf`);
177
+ const body = Buffer.from(payload);
178
+ const client = endpoint.protocol === "https:" ? https : http;
179
+ const req = client.request(endpoint, {
180
+ method: "POST",
181
+ headers: {
182
+ "content-type": "application/json",
183
+ "content-length": body.byteLength,
184
+ },
185
+ }, (res) => {
186
+ res.resume();
187
+ });
188
+ req.on("error", (err) => {
189
+ console.error(`[observable-graylog] Failed to send HTTP log: ${err.message}`);
190
+ });
191
+ req.end(body);
192
+ }
150
193
  // Logging methods
151
194
  debug(trace, pluginName, message, meta) {
152
195
  if (this.mode === "production" || !this.config.levels.debug) {
@@ -184,6 +227,5 @@ export class Plugin extends BSBObservable {
184
227
  }
185
228
  dispose() {
186
229
  this.isDisposed = true;
187
- // gelf-pro doesn't require explicit cleanup
188
230
  }
189
231
  }
@@ -1,7 +1,158 @@
1
1
  {
2
2
  "pluginName": "observable-graylog",
3
- "version": "9.0.0",
4
3
  "events": {},
4
+ "version": "9.6.12",
5
+ "configSchema": {
6
+ "anyvaliVersion": "1.0",
7
+ "schemaVersion": "1.1",
8
+ "root": {
9
+ "kind": "object",
10
+ "properties": {
11
+ "host": {
12
+ "kind": "string",
13
+ "default": "localhost",
14
+ "metadata": {
15
+ "description": "Graylog server hostname"
16
+ }
17
+ },
18
+ "port": {
19
+ "kind": "int32",
20
+ "min": 1,
21
+ "max": 65535,
22
+ "default": 12201,
23
+ "metadata": {
24
+ "description": "Graylog GELF server port"
25
+ }
26
+ },
27
+ "protocol": {
28
+ "kind": "enum",
29
+ "values": [
30
+ "udp",
31
+ "tcp",
32
+ "http"
33
+ ],
34
+ "default": "udp",
35
+ "metadata": {
36
+ "description": "Transport protocol used to send GELF messages"
37
+ }
38
+ },
39
+ "httpEndpoint": {
40
+ "kind": "optional",
41
+ "inner": {
42
+ "kind": "string",
43
+ "format": "url"
44
+ },
45
+ "metadata": {
46
+ "description": "HTTP GELF endpoint used when protocol is http"
47
+ }
48
+ },
49
+ "facility": {
50
+ "kind": "string",
51
+ "default": "bsb",
52
+ "metadata": {
53
+ "description": "GELF facility field value"
54
+ }
55
+ },
56
+ "additionalFields": {
57
+ "kind": "optional",
58
+ "inner": {
59
+ "kind": "record",
60
+ "valueSchema": {
61
+ "kind": "union",
62
+ "variants": [
63
+ {
64
+ "kind": "string",
65
+ "metadata": {
66
+ "description": "String additional field value"
67
+ }
68
+ },
69
+ {
70
+ "kind": "number",
71
+ "metadata": {
72
+ "description": "Numeric additional field value"
73
+ }
74
+ },
75
+ {
76
+ "kind": "bool",
77
+ "metadata": {
78
+ "description": "Boolean additional field value"
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ },
84
+ "default": {},
85
+ "metadata": {
86
+ "description": "Additional static GELF fields added to every log message"
87
+ }
88
+ },
89
+ "compress": {
90
+ "kind": "bool",
91
+ "default": true,
92
+ "metadata": {
93
+ "description": "Whether UDP GELF message compression is enabled"
94
+ }
95
+ },
96
+ "levels": {
97
+ "kind": "object",
98
+ "properties": {
99
+ "debug": {
100
+ "kind": "bool",
101
+ "default": true,
102
+ "metadata": {
103
+ "description": "Whether debug logs are sent to Graylog"
104
+ }
105
+ },
106
+ "info": {
107
+ "kind": "bool",
108
+ "default": true,
109
+ "metadata": {
110
+ "description": "Whether info logs are sent to Graylog"
111
+ }
112
+ },
113
+ "warn": {
114
+ "kind": "bool",
115
+ "default": true,
116
+ "metadata": {
117
+ "description": "Whether warning logs are sent to Graylog"
118
+ }
119
+ },
120
+ "error": {
121
+ "kind": "bool",
122
+ "default": true,
123
+ "metadata": {
124
+ "description": "Whether error logs are sent to Graylog"
125
+ }
126
+ }
127
+ },
128
+ "required": [
129
+ "debug",
130
+ "info",
131
+ "warn",
132
+ "error"
133
+ ],
134
+ "unknownKeys": "strip",
135
+ "metadata": {
136
+ "description": "Log level enablement"
137
+ }
138
+ }
139
+ },
140
+ "required": [
141
+ "host",
142
+ "port",
143
+ "protocol",
144
+ "facility",
145
+ "compress",
146
+ "levels"
147
+ ],
148
+ "unknownKeys": "strip",
149
+ "metadata": {
150
+ "description": "Graylog observable plugin configuration"
151
+ }
152
+ },
153
+ "definitions": {},
154
+ "extensions": {}
155
+ },
5
156
  "pluginType": "observable",
6
157
  "capabilities": {
7
158
  "logging": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "observable-graylog",
3
3
  "name": "observable-graylog",
4
- "version": "9.0.0",
4
+ "version": "9.6.12",
5
5
  "description": "Graylog GELF observable plugin for centralized log ingestion",
6
6
  "category": "observable",
7
7
  "tags": [
@@ -14,5 +14,162 @@
14
14
  "./docs/plugin.md"
15
15
  ],
16
16
  "dependencies": [],
17
- "image": "./observable-graylog.png"
17
+ "author": {
18
+ "name": "BetterCorp (PTY) Ltd",
19
+ "email": "ninja@bettercorp.dev",
20
+ "url": "https://bettercorp.dev/"
21
+ },
22
+ "license": "(AGPL-3.0-only OR Commercial)",
23
+ "image": "./observable-graylog.png",
24
+ "configSchema": {
25
+ "anyvaliVersion": "1.0",
26
+ "schemaVersion": "1.1",
27
+ "root": {
28
+ "kind": "object",
29
+ "properties": {
30
+ "host": {
31
+ "kind": "string",
32
+ "default": "localhost",
33
+ "metadata": {
34
+ "description": "Graylog server hostname"
35
+ }
36
+ },
37
+ "port": {
38
+ "kind": "int32",
39
+ "min": 1,
40
+ "max": 65535,
41
+ "default": 12201,
42
+ "metadata": {
43
+ "description": "Graylog GELF server port"
44
+ }
45
+ },
46
+ "protocol": {
47
+ "kind": "enum",
48
+ "values": [
49
+ "udp",
50
+ "tcp",
51
+ "http"
52
+ ],
53
+ "default": "udp",
54
+ "metadata": {
55
+ "description": "Transport protocol used to send GELF messages"
56
+ }
57
+ },
58
+ "httpEndpoint": {
59
+ "kind": "optional",
60
+ "inner": {
61
+ "kind": "string",
62
+ "format": "url"
63
+ },
64
+ "metadata": {
65
+ "description": "HTTP GELF endpoint used when protocol is http"
66
+ }
67
+ },
68
+ "facility": {
69
+ "kind": "string",
70
+ "default": "bsb",
71
+ "metadata": {
72
+ "description": "GELF facility field value"
73
+ }
74
+ },
75
+ "additionalFields": {
76
+ "kind": "optional",
77
+ "inner": {
78
+ "kind": "record",
79
+ "valueSchema": {
80
+ "kind": "union",
81
+ "variants": [
82
+ {
83
+ "kind": "string",
84
+ "metadata": {
85
+ "description": "String additional field value"
86
+ }
87
+ },
88
+ {
89
+ "kind": "number",
90
+ "metadata": {
91
+ "description": "Numeric additional field value"
92
+ }
93
+ },
94
+ {
95
+ "kind": "bool",
96
+ "metadata": {
97
+ "description": "Boolean additional field value"
98
+ }
99
+ }
100
+ ]
101
+ }
102
+ },
103
+ "default": {},
104
+ "metadata": {
105
+ "description": "Additional static GELF fields added to every log message"
106
+ }
107
+ },
108
+ "compress": {
109
+ "kind": "bool",
110
+ "default": true,
111
+ "metadata": {
112
+ "description": "Whether UDP GELF message compression is enabled"
113
+ }
114
+ },
115
+ "levels": {
116
+ "kind": "object",
117
+ "properties": {
118
+ "debug": {
119
+ "kind": "bool",
120
+ "default": true,
121
+ "metadata": {
122
+ "description": "Whether debug logs are sent to Graylog"
123
+ }
124
+ },
125
+ "info": {
126
+ "kind": "bool",
127
+ "default": true,
128
+ "metadata": {
129
+ "description": "Whether info logs are sent to Graylog"
130
+ }
131
+ },
132
+ "warn": {
133
+ "kind": "bool",
134
+ "default": true,
135
+ "metadata": {
136
+ "description": "Whether warning logs are sent to Graylog"
137
+ }
138
+ },
139
+ "error": {
140
+ "kind": "bool",
141
+ "default": true,
142
+ "metadata": {
143
+ "description": "Whether error logs are sent to Graylog"
144
+ }
145
+ }
146
+ },
147
+ "required": [
148
+ "debug",
149
+ "info",
150
+ "warn",
151
+ "error"
152
+ ],
153
+ "unknownKeys": "strip",
154
+ "metadata": {
155
+ "description": "Log level enablement"
156
+ }
157
+ }
158
+ },
159
+ "required": [
160
+ "host",
161
+ "port",
162
+ "protocol",
163
+ "facility",
164
+ "compress",
165
+ "levels"
166
+ ],
167
+ "unknownKeys": "strip",
168
+ "metadata": {
169
+ "description": "Graylog observable plugin configuration"
170
+ }
171
+ },
172
+ "definitions": {},
173
+ "extensions": {}
174
+ }
18
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsb/observable-graylog",
3
- "version": "9.1.2",
3
+ "version": "9.6.12",
4
4
  "type": "module",
5
5
  "license": "(AGPL-3.0-only OR Commercial)",
6
6
  "author": {
@@ -38,20 +38,19 @@
38
38
  "prepublishOnly": "npm run build"
39
39
  },
40
40
  "peerDependencies": {
41
- "@bsb/base": "^9.0.0"
41
+ "@bsb/base": "^9.6.12"
42
42
  },
43
43
  "dependencies": {
44
- "@anyvali/js": "^0.2.0",
45
- "gelf-pro": "^1.3.2"
44
+ "anyvali": "^1.0.6"
46
45
  },
47
46
  "devDependencies": {
48
- "@bsb/base": "^9.0.0",
49
- "@types/node": "^25.0.0",
50
- "mocha": "^11.0.0",
51
- "typescript": "^5.9.0"
47
+ "@bsb/base": "^9.6.12",
48
+ "@types/node": "^25.6.0",
49
+ "mocha": "^11.7.6",
50
+ "typescript": "^6.0.3"
52
51
  },
53
52
  "engines": {
54
- "node": ">=23.0.0",
53
+ "node": ">=24.0.0",
55
54
  "npm": ">=11.0.0"
56
55
  },
57
56
  "homepage": "https://io.bsbcode.dev/packages/nodejs/@bsb/observable-graylog"