geonix 1.23.8 → 1.30.4

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/src/Gateway.js CHANGED
@@ -1,20 +1,38 @@
1
1
  import { connection } from "./Connection.js";
2
2
  import { registry } from "./Registry.js";
3
- import { cleanupWebsocketUrl, createTCPServer, GeonixVersion, picoid, proxyHttp, sleep } from "./Util.js";
3
+ import { cleanupWebsocketUrl, createTCPServer, GeonixVersion, hash, picoid, proxyHttp, sleep } from "./Util.js";
4
4
  import express, { Router } from "express";
5
5
  import { Request } from "./Request.js";
6
- import { HEALTH_CHECK_ENDPOINT } from "./WebServer.js";
7
6
  import expressWs from "express-ws";
8
- import querystring from "querystring";
9
7
  import semver from "semver";
10
8
  import { WebSocket } from "ws";
11
9
  import { logger } from "./Logger.js";
12
10
 
13
- const DEBUG_ENDPOINT = "/lZ6jD2eC3iP0zB3jJ1yJ9pM8gG3yI3vS";
14
11
  const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
15
12
 
13
+ const MAX_SESSIONS = 16384;
14
+
15
+ // Real client IP — checks well-known CDN/proxy headers before falling back to socket address
16
+ function getClientIp(req) {
17
+ const header =
18
+ req.headers["cf-connecting-ip"] || // Cloudflare
19
+ req.headers["true-client-ip"] || // Cloudflare Enterprise / Akamai
20
+ req.headers["x-real-ip"] || // nginx
21
+ req.headers["x-forwarded-for"]; // standard (may be comma-separated list)
22
+
23
+ if (header) {
24
+ return header.split(",")[0].trim();
25
+ }
26
+ return req.socket?.remoteAddress || "unknown";
27
+ }
28
+
29
+ // Deterministic index into an array based on a string key
30
+ function stableIndex(str, length) {
31
+ return parseInt(hash(str).slice(0, 8), 16) % length;
32
+ }
33
+
16
34
  const requestLogger = (req, res, next) => {
17
- logger.info(`HTTP ${req.method} ${req.url}`);
35
+ logger.debug(`HTTP ${req.method} ${req.path}`);
18
36
 
19
37
  next();
20
38
  };
@@ -23,16 +41,26 @@ const stats = {
23
41
  requests: 0,
24
42
  proxied: 0,
25
43
  proxied_over_nats: 0,
26
- debug_requests: 0
44
+ debug_requests: 0,
27
45
  };
28
46
 
29
47
  const defaultOpts = {
30
- beforeRequest: (_req, _res) => { },
31
- afterRequest: (_req, _res) => { }
48
+ beforeRequest: (_req, _res) => {},
49
+ afterRequest: (_req, _res) => {},
32
50
  };
33
51
 
52
+ /**
53
+ * HTTP gateway that automatically discovers Geonix services via the registry and reverse-proxies
54
+ * incoming HTTP and WebSocket requests to the appropriate service instance. Routes are rebuilt
55
+ * dynamically as services join and leave the bus.
56
+ */
34
57
  export class Gateway {
35
-
58
+ /**
59
+ * Creates and starts a new Gateway instance.
60
+ *
61
+ * @param {object} [opts] - Gateway options (e.g. `cors`, `beforeRequest`, `afterRequest`).
62
+ * @returns {Gateway}
63
+ */
36
64
  static start(opts) {
37
65
  return new Gateway(opts);
38
66
  }
@@ -46,16 +74,19 @@ export class Gateway {
46
74
 
47
75
  #rebuildRouter = false;
48
76
  #buildRouterRunning = false;
77
+ #rebuildRouterInterval = null;
49
78
  #endpoints = [];
50
79
 
51
80
  #registry = {};
81
+ #server;
82
+ #sessions = new Map(); // _gx token → backend address
52
83
 
53
84
  constructor(opts) {
54
85
  expressWs(this.#api);
55
86
 
56
87
  this.#opts = { ...this.#opts, ...opts };
57
88
 
58
- this.#start();
89
+ this.#start().catch((e) => logger.error("gx.gateway.start:", e));
59
90
  }
60
91
 
61
92
  async #start(port = 8080) {
@@ -63,36 +94,59 @@ export class Gateway {
63
94
 
64
95
  await connection.waitUntilReady();
65
96
 
66
- this.#port = process.env.PORT || port;
67
- this.#api.listen(this.#port);
68
-
69
- logger.debug(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`);
97
+ this.#port = process.env.GX_PORT || process.env.PORT || port;
70
98
 
71
99
  // logging
72
100
  this.#api.use(requestLogger);
73
101
 
74
102
  // cors
75
103
  this.#api.use((req, res, next) => {
104
+ const cors = this.#opts.cors;
76
105
  const origin = req.headers["origin"];
77
106
  const allMethods = "GET,PUT,POST,DELETE,OPTIONS,HEAD";
78
- const allHeaders = "*";
79
107
  const requestMethod = req.headers["access-control-request-method"];
80
108
  const requestHeaders = req.headers["access-control-request-headers"];
81
109
 
82
- res.set("Access-Control-Allow-Credentials", "true");
83
- res.set("Access-Control-Allow-Origin", origin || "*");
84
- res.set("Access-Control-Allow-Methods", requestMethod || allMethods);
85
110
  res.set("Allow", requestMethod || allMethods);
86
- res.set("Access-Control-Allow-Headers", requestHeaders || allHeaders);
111
+
112
+ if (!cors) {
113
+ // off — no CORS headers
114
+ } else if (cors === "*") {
115
+ // wildcard — all origins, no credentials
116
+ res.set("Access-Control-Allow-Origin", "*");
117
+ res.set("Access-Control-Allow-Methods", requestMethod || allMethods);
118
+ res.set("Access-Control-Allow-Headers", requestHeaders || "*");
119
+ } else if (Array.isArray(cors)) {
120
+ // allowlist — credentials only for listed origins
121
+ if (origin && cors.includes(origin)) {
122
+ res.set("Access-Control-Allow-Credentials", "true");
123
+ res.set("Access-Control-Allow-Origin", origin);
124
+ res.set("Access-Control-Allow-Methods", requestMethod || allMethods);
125
+ res.set("Access-Control-Allow-Headers", requestHeaders || "*");
126
+ }
127
+ } else if (cors === true) {
128
+ // open — reflect any origin with credentials
129
+ if (origin) {
130
+ res.set("Access-Control-Allow-Credentials", "true");
131
+ res.set("Access-Control-Allow-Origin", origin);
132
+ res.set("Access-Control-Allow-Methods", requestMethod || allMethods);
133
+ res.set("Access-Control-Allow-Headers", requestHeaders || "*");
134
+ }
135
+ }
87
136
 
88
137
  next();
89
138
  });
90
139
 
91
- // debug router (only available in non-production environments)
92
- if (process.env.NODE_ENV !== "production") {
93
- this.#api.use(DEBUG_ENDPOINT, this.#debugRouter());
140
+ // debug router (only mounted when GX_DEBUG_ENDPOINT is set)
141
+ if (process.env.GX_DEBUG_ENDPOINT) {
142
+ this.#api.use(process.env.GX_DEBUG_ENDPOINT, this.#debugRouter());
94
143
  }
95
144
 
145
+ // block internal gx namespace from being proxied to external clients
146
+ this.#api.all("/!!_gx/*", (req, res) => {
147
+ res.status(403).json({ error: 403, source: "gw" });
148
+ });
149
+
96
150
  this.#api.use((req, res, next) => {
97
151
  if (this.#opts.beforeRequest) {
98
152
  this.#opts.beforeRequest(req, res);
@@ -126,11 +180,14 @@ export class Gateway {
126
180
  this.#api.all("*", (req, res) => {
127
181
  res.status(404).send({
128
182
  error: 404,
129
- source: "gw"
183
+ source: "gw",
130
184
  });
131
185
  });
132
186
 
133
- setInterval(() => {
187
+ this.#server = this.#api.listen(this.#port);
188
+ logger.info(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`);
189
+
190
+ this.#rebuildRouterInterval = setInterval(() => {
134
191
  if (this.#rebuildRouter) {
135
192
  this.#buildRouter();
136
193
  }
@@ -141,7 +198,7 @@ export class Gateway {
141
198
  // send keeplive to check if connection is still alive
142
199
  await connection.publish("gx.gateway.keepalive", Date.now());
143
200
 
144
- await this.#handleAddedServics();
201
+ await this.#handleAddedServices();
145
202
  await this.#handleRemovedServices();
146
203
  await sleep(1000);
147
204
 
@@ -153,42 +210,31 @@ export class Gateway {
153
210
  }
154
211
  }
155
212
 
156
- await this.#api.close();
213
+ this.#server?.close();
157
214
 
158
215
  // terminate process
159
- logger.debug("geonix.gateway: stopped");
216
+ logger.info("geonix.gateway: stopped");
160
217
  }
161
218
 
162
- async #handleAddedServics() {
219
+ async #handleAddedServices() {
163
220
  let entries = Object.values(registry.getEntries());
164
221
 
165
222
  const processEntry = async (entry) => {
166
- if (this.#registry[entry.i] !== undefined) { return false; }
223
+ const existing = this.#registry[entry.i];
167
224
 
168
- logger.info(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`);
169
-
170
- // figure out if endpoints is reachable via direct http call
171
- let backend;
172
- if (entry.a) {
173
- for (let address of entry.a) {
174
- try {
175
- const ac = new AbortController();
176
- const timeout = setTimeout(() => ac.abort(), 500);
177
- const result = await (await fetch(`http://${address}${HEALTH_CHECK_ENDPOINT}`, { signal: ac.signal })).json();
178
- clearTimeout(timeout);
179
- if (result.status === "healthy" && result.services?.includes(entry.n)) {
180
- backend = address;
181
- logger.info(`${entry.n}@${entry.v} (#${entry.i}) directly reachable @ ${address}`);
182
- break;
183
- }
184
- } catch {
185
- // silently ignore errors
186
- }
225
+ if (existing !== undefined) {
226
+ // trigger rebuild if a direct entry has gained new addresses since last seen
227
+ if (!existing.proxy && entry.a?.length !== existing.knownAddressCount) {
228
+ existing.knownAddressCount = entry.a.length;
229
+ this.#rebuildRouter = true;
187
230
  }
231
+ return false;
188
232
  }
189
233
 
234
+ logger.info(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`);
235
+
190
236
  let proxy;
191
- if (!backend) {
237
+ if (!entry.a?.length) {
192
238
  // create proxy over nats
193
239
  proxy = await createTCPServer(async (client) => {
194
240
  const streamId = picoid();
@@ -200,16 +246,15 @@ export class Gateway {
200
246
  logger.error("nats.proxy.error", e);
201
247
  client.destroy();
202
248
  }
203
- }, 50000, 10000);
204
- backend = `127.0.0.1:${proxy.port}`;
249
+ });
205
250
  }
206
251
 
207
- this.#registry[entry.i] = { entry, proxy, backend };
252
+ this.#registry[entry.i] = { entry, proxy, knownAddressCount: entry.a?.length ?? 0 };
208
253
 
209
254
  return true;
210
255
  };
211
256
 
212
- entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
257
+ entries = (await Promise.all(entries.map(processEntry))).filter((result) => result === true);
213
258
 
214
259
  if (entries.length > 0) {
215
260
  this.#rebuildRouter = true;
@@ -241,7 +286,7 @@ export class Gateway {
241
286
  });
242
287
 
243
288
  router.get("/services", (req, res) => {
244
- const services = Object.values(registry.getEntries()).map(e => (`${e.n}@${e.v}`));
289
+ const services = Object.values(registry.getEntries()).map((e) => `${e.n}@${e.v}`);
245
290
  services.sort();
246
291
  res.send(services);
247
292
  });
@@ -268,12 +313,11 @@ export class Gateway {
268
313
  node: {
269
314
  version: process.version,
270
315
  platform: process.platform,
271
- arch: process.arch
316
+ arch: process.arch,
272
317
  },
273
- env: process.env,
274
318
  mem: process.memoryUsage(),
275
319
  rss: process.memoryUsage.rss(),
276
- cpu: process.cpuUsage()
320
+ cpu: process.cpuUsage(),
277
321
  });
278
322
  });
279
323
 
@@ -293,42 +337,46 @@ export class Gateway {
293
337
  connection.unsubscribe(ingress);
294
338
  connection.publish(`gx2.stream.${streamId}.c`, Buffer.from("end"));
295
339
  });
296
- client.on("error", (_error) => {
297
- // silently ignore errors
340
+ client.on("error", (e) => {
341
+ logger.debug("nats.proxy.client.error:", e);
298
342
  });
299
343
 
300
344
  const dataLoop = async () => {
301
- for await (const event of ingress) { client.write(event.data); }
345
+ for await (const event of ingress) {
346
+ client.write(event.data);
347
+ }
302
348
  };
303
349
 
304
- dataLoop();
350
+ dataLoop().catch((e) => logger.error("nats.proxy.dataLoop:", e));
305
351
  }
306
352
  }
307
353
 
308
354
  /**
309
355
  * Proxies websocket connection
310
- *
311
- * @param {string} target
312
- * @param {Readable} inbound
313
- * @param {Request} req
356
+ *
357
+ * @param {string} target
358
+ * @param {Readable} inbound
359
+ * @param {Request} req
314
360
  */
315
- #proxyWebsocketOverNats(target, inbound, req) {
361
+ #proxyWebsocket(target, inbound, req) {
316
362
  try {
317
- const backend = new WebSocket(target, {
318
- headers: {
319
- ...req.headers
320
- }
363
+ const backend = new WebSocket(target, { headers: { ...req.headers } });
364
+
365
+ backend.on("error", (e) => {
366
+ logger.error("proxy.ws.backend.error:", e);
367
+ inbound.close();
321
368
  });
322
369
 
323
370
  backend.on("open", () => {
324
371
  backend.on("message", (data, isBinary) => inbound.send(isBinary ? data : data.toString()));
325
- inbound.on("message", data => backend.send(data));
372
+ inbound.on("message", (data) => backend.send(data));
326
373
 
327
374
  backend.on("close", () => inbound.close());
328
375
  inbound.on("close", () => backend.close());
329
376
  });
330
377
  } catch (e) {
331
- logger.error(e);
378
+ logger.error("proxy.ws.backend.error:", e);
379
+ inbound.close();
332
380
  }
333
381
  }
334
382
 
@@ -348,14 +396,27 @@ export class Gateway {
348
396
 
349
397
  const endpoints = [];
350
398
 
351
- for (let { entry, backend } of entries) {
399
+ for (let { entry, proxy } of entries) {
400
+ const backendAddr = proxy
401
+ ? `127.0.0.1:${proxy.port}`
402
+ : entry.a?.[Math.floor(Math.random() * entry.a.length)];
403
+
404
+ if (!backendAddr) {
405
+ continue;
406
+ }
407
+
352
408
  // generate global endpoint list
353
409
  for (let e of entry.m) {
354
- if (endpointMatcher.test(e)) {
355
- const endpoint = endpointMatcher.exec(e).groups;
410
+ const endpointMatch = endpointMatcher.exec(e);
411
+ if (endpointMatch) {
412
+ const endpoint = endpointMatch.groups;
413
+ const parsed = endpoint.options
414
+ ? Object.fromEntries(new URLSearchParams(endpoint.options))
415
+ : {};
416
+ const parsedOrder = parseInt(parsed.order, 10);
356
417
  let options = {
357
- order: 100,
358
- ...(endpoint.options ? querystring.parse(endpoint.options) : {})
418
+ ...parsed,
419
+ order: Number.isNaN(parsedOrder) ? 100 : parsedOrder,
359
420
  };
360
421
 
361
422
  try {
@@ -364,7 +425,7 @@ export class Gateway {
364
425
  version: semver.coerce(entry.v).version,
365
426
  options,
366
427
  endpoint,
367
- backend: [backend]
428
+ backend: [backendAddr],
368
429
  });
369
430
  } catch (e) {
370
431
  logger.error("gateway.buildRouter.error:", entry);
@@ -381,7 +442,10 @@ export class Gateway {
381
442
  const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
382
443
 
383
444
  for (let n = 0; n < index; n++) {
384
- if (`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url && endpoints[n].version === version) {
445
+ if (
446
+ `${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url &&
447
+ endpoints[n].version === version
448
+ ) {
385
449
  endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
386
450
  endpoints.splice(index, 1);
387
451
  break;
@@ -391,28 +455,45 @@ export class Gateway {
391
455
 
392
456
  // sort endpoints by order, if there is one
393
457
  endpoints.sort((a, b) => semver.rcompare(a.version, b.version));
394
- endpoints.sort((a, b) => parseInt(a.options.order) - parseInt(b.options.order));
458
+ endpoints.sort((a, b) => a.options.order - b.options.order);
395
459
 
396
460
  this.#endpoints = endpoints;
397
461
 
398
462
  // build the router
399
463
  for (let endpoint of endpoints) {
400
464
  let { verb, url: uri } = endpoint.endpoint;
401
- let backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
402
-
403
465
  verb = verb.toLowerCase();
404
466
 
405
467
  if (verb === "ws") {
406
468
  router.ws(uri, (ws, req) => {
407
- let target = cleanupWebsocketUrl(`ws://${backend}${req.originalUrl}`);
469
+ const backends = endpoint.backend;
470
+ const gxToken = req.query?._gx;
471
+ let selectedBackend;
472
+
473
+ if (gxToken) {
474
+ const stored = this.#sessions.get(gxToken);
475
+ if (stored && backends.includes(stored)) {
476
+ selectedBackend = stored;
477
+ } else {
478
+ // backend gone or first time seeing this token
479
+ selectedBackend = backends[stableIndex(getClientIp(req), backends.length)];
480
+ if (this.#sessions.size >= MAX_SESSIONS) {
481
+ this.#sessions.delete(this.#sessions.keys().next().value);
482
+ }
483
+ this.#sessions.set(gxToken, selectedBackend);
484
+ }
485
+ } else {
486
+ selectedBackend = backends[stableIndex(getClientIp(req), backends.length)];
487
+ }
408
488
 
489
+ const target = cleanupWebsocketUrl(`ws://${selectedBackend}${req.originalUrl}`);
409
490
  logger.debug("proxy.web.ws.to:", target);
410
- this.#proxyWebsocketOverNats(target, ws, req);
491
+ this.#proxyWebsocket(target, ws, req);
411
492
  });
412
493
  } else {
413
494
  router[verb](uri, async (req, res, _next) => {
414
495
  stats.proxied++;
415
- backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
496
+ const backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
416
497
 
417
498
  try {
418
499
  logger.debug("proxy.web.to:", backend + req.originalUrl);
@@ -430,8 +511,12 @@ export class Gateway {
430
511
  }
431
512
  }
432
513
 
433
- async stop() {
514
+ /**
515
+ * Stops the gateway loop and closes the HTTP server.
516
+ * @returns {void}
517
+ */
518
+ stop() {
434
519
  this.#isActive = false;
520
+ clearInterval(this.#rebuildRouterInterval);
435
521
  }
436
-
437
522
  }
package/src/Logger.js CHANGED
@@ -1,34 +1,124 @@
1
+ const LEVELS = { debug: 0, info: 1, warn: 2, error: 3, none: 4 };
2
+ const TAGS = { debug: "DBG", info: "INF", warn: "WRN", error: "ERR" };
3
+
4
+ const LEVEL = LEVELS[process.env.GX_LOG_LEVEL] ?? LEVELS.info;
5
+ const FORMAT = process.env.GX_LOG_FORMAT === "json" ? "json" : "text";
6
+
1
7
  const defaultLoggerOptions = {
2
- timestamp: true
8
+ timestamp: true,
3
9
  };
4
10
 
5
- export class Logger {
11
+ function serialize(val) {
12
+ if (val instanceof Error) {
13
+ return val.stack || val.message;
14
+ }
15
+ if (typeof val === "object" && val !== null) {
16
+ return JSON.stringify(val);
17
+ }
18
+ return String(val);
19
+ }
6
20
 
21
+ /**
22
+ * Lightweight structured logger with configurable level filtering and output format.
23
+ * Each log method prefixes the message with an ISO timestamp (unless disabled) and a
24
+ * severity tag (`INF`, `WRN`, `ERR`, `DBG`). The active level is controlled by the
25
+ * `GX_LOG_LEVEL` environment variable (`debug` | `info` | `warn` | `error`).
26
+ * The output format is controlled by `GX_LOG_FORMAT` (`text` | `json`, default `text`).
27
+ */
28
+ export class Logger {
7
29
  #options = defaultLoggerOptions;
30
+ #level = LEVEL;
31
+ #format = FORMAT;
8
32
 
33
+ /**
34
+ * @param {object} [options] - Logger options.
35
+ * @param {boolean} [options.timestamp=true] - Whether to prepend an ISO timestamp to each line (text format only).
36
+ * @param {'debug'|'info'|'warn'|'error'|'none'} [options.level] - Minimum level to emit. Use `none` to suppress all output. Overrides `GX_LOG_LEVEL`.
37
+ * @param {'text'|'json'} [options.format] - Output format. Overrides `GX_LOG_FORMAT`.
38
+ */
9
39
  constructor(options) {
10
40
  this.#options = { ...this.#options, ...options };
41
+ if (options?.level !== undefined) {
42
+ this.#level = LEVELS[options.level] ?? LEVEL;
43
+ }
44
+ if (options?.format !== undefined) {
45
+ this.#format = options.format === "json" ? "json" : "text";
46
+ }
11
47
  }
12
48
 
13
- #log(...args) {
14
- const ts = this.#options.timestamp ? new Date().toISOString() : undefined;
15
-
16
- // eslint-disable-next-line no-console
17
- console.log(...[ts, ...args].filter($ => $));
49
+ #log(level, ...args) {
50
+ const stream = level === "error" ? process.stderr : process.stdout;
51
+ if (this.#format === "json") {
52
+ stream.write(
53
+ JSON.stringify({ time: new Date().toISOString(), level, msg: args.map(serialize).join(" ") }) + "\n",
54
+ );
55
+ } else {
56
+ const ts = this.#options.timestamp ? new Date().toISOString() : undefined;
57
+ stream.write(
58
+ [ts, TAGS[level], ...args]
59
+ .filter(($) => $ !== undefined)
60
+ .map(serialize)
61
+ .join(" ") + "\n",
62
+ );
63
+ }
18
64
  }
19
65
 
66
+ /**
67
+ * Emits an informational log line.
68
+ * @param {...any} args - Values to log.
69
+ * @returns {void}
70
+ */
20
71
  info(...args) {
21
- this.#log("INF", ...args);
72
+ if (LEVELS.info >= this.#level) {
73
+ this.#log("info", ...args);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Emits a warning log line.
79
+ * @param {...any} args - Values to log.
80
+ * @returns {void}
81
+ */
82
+ warn(...args) {
83
+ if (LEVELS.warn >= this.#level) {
84
+ this.#log("warn", ...args);
85
+ }
22
86
  }
23
87
 
88
+ /**
89
+ * Emits an error log line.
90
+ * @param {...any} args - Values to log.
91
+ * @returns {void}
92
+ */
24
93
  error(...args) {
25
- this.#log("ERR", ...args);
94
+ if (LEVELS.error >= this.#level) {
95
+ this.#log("error", ...args);
96
+ }
26
97
  }
27
98
 
99
+ /**
100
+ * Emits a debug log line. Only shown when the active level is `debug`.
101
+ * @param {...any} args - Values to log.
102
+ * @returns {void}
103
+ */
28
104
  debug(...args) {
29
- this.#log("DBG", ...args);
105
+ if (LEVELS.debug >= this.#level) {
106
+ this.#log("debug", ...args);
107
+ }
30
108
  }
31
109
 
110
+ setLevel(level) {
111
+ this.#level = LEVELS[level] ?? LEVEL;
112
+ }
113
+
114
+ setFormat(format) {
115
+ this.#format = format === "json" ? "json" : "text";
116
+ }
32
117
  }
33
118
 
34
- export const logger = new Logger();
119
+ /**
120
+ * Default {@link Logger} instance used throughout the Geonix internals.
121
+ *
122
+ * @type {Logger}
123
+ */
124
+ export const logger = new Logger();