@alt-javascript/camel-lite-component-log 1.0.2

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 ADDED
@@ -0,0 +1,53 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ ## What
4
+
5
+ Structured logging via [`@alt-javascript/logger`](https://www.npmjs.com/package/@alt-javascript/logger). Sends the exchange body (stringified if not already a string) to the named logger at the configured level. Producer-only — no consumer.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install camel-lite-component-log @alt-javascript/logger @alt-javascript/config @alt-javascript/common
11
+ ```
12
+
13
+ ## URI Syntax
14
+
15
+ ```
16
+ log:loggerName[?level=info]
17
+ ```
18
+
19
+ | Segment / Parameter | Default | Description |
20
+ |---------------------|---------|-------------|
21
+ | `loggerName` | *(required)* | Logger category name (case-preserved). Passed directly to `LoggerFactory.getLogger`. |
22
+ | `level` | `info` | Log level: `trace`, `debug`, `info`, `warn`, or `error`. |
23
+
24
+ ## Usage
25
+
26
+ ```js
27
+ import { CamelContext } from 'camel-lite-core';
28
+ import { LogComponent } from 'camel-lite-component-log';
29
+
30
+ const context = new CamelContext();
31
+ context.addComponent('log', new LogComponent());
32
+
33
+ context.addRoutes({
34
+ configure(ctx) {
35
+ ctx.from('direct:ingest')
36
+ .to('log:com.example.ingest?level=debug')
37
+ .process(exchange => {
38
+ // continue processing after log
39
+ });
40
+ }
41
+ });
42
+
43
+ await context.start();
44
+
45
+ const template = context.createProducerTemplate();
46
+ await template.sendBody('direct:ingest', { id: 1, value: 'hello' });
47
+
48
+ await context.stop();
49
+ ```
50
+
51
+ ## See Also
52
+
53
+ [camel-lite — root README](../../README.md)
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@alt-javascript/camel-lite-component-log",
3
+ "version": "1.0.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.js"
7
+ },
8
+ "dependencies": {
9
+ "@alt-javascript/common": "^3.0.7",
10
+ "@alt-javascript/config": "^3.0.7",
11
+ "@alt-javascript/logger": "^3.0.7",
12
+ "@alt-javascript/camel-lite-core": "1.0.2"
13
+ },
14
+ "scripts": {
15
+ "test": "node --test"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/alt-javascript/camel-lite"
20
+ },
21
+ "author": "Craig Parravicini",
22
+ "contributors": [
23
+ "Claude (Anthropic)",
24
+ "Apache Camel — design inspiration and pattern source"
25
+ ],
26
+ "keywords": [
27
+ "alt-javascript",
28
+ "camel",
29
+ "camel-lite",
30
+ "eai",
31
+ "eip",
32
+ "integration",
33
+ "log",
34
+ "logging",
35
+ "component"
36
+ ],
37
+ "publishConfig": {
38
+ "registry": "https://registry.npmjs.org/",
39
+ "access": "public"
40
+ }
41
+ }
@@ -0,0 +1,11 @@
1
+ import { Component } from '@alt-javascript/camel-lite-core';
2
+ import LogEndpoint from './LogEndpoint.js';
3
+
4
+ class LogComponent extends Component {
5
+ createEndpoint(uri, remaining, parameters, context) {
6
+ return new LogEndpoint(uri);
7
+ }
8
+ }
9
+
10
+ export { LogComponent };
11
+ export default LogComponent;
@@ -0,0 +1,65 @@
1
+ import { Endpoint } from '@alt-javascript/camel-lite-core';
2
+ import LogProducer from './LogProducer.js';
3
+
4
+ class LogEndpoint extends Endpoint {
5
+ #uri;
6
+ #level;
7
+ #showBody;
8
+ #showHeaders;
9
+ #loggerName;
10
+
11
+ constructor(uri) {
12
+ super();
13
+ this.#uri = uri;
14
+
15
+ // Strip scheme: 'log:output?level=info' → 'output?level=info'
16
+ const remaining = uri.slice(uri.indexOf(':') + 1);
17
+
18
+ // Split name from query string manually to preserve case
19
+ const qIdx = remaining.indexOf('?');
20
+ const namePart = qIdx >= 0 ? remaining.slice(0, qIdx) : remaining;
21
+ const queryPart = qIdx >= 0 ? remaining.slice(qIdx + 1) : '';
22
+ const params = new URLSearchParams(queryPart);
23
+
24
+ this.#loggerName = namePart || 'log';
25
+ this.#level = (params.get('level') ?? 'info').toLowerCase();
26
+ this.#showBody = params.get('showBody') !== 'false';
27
+ this.#showHeaders = params.get('showHeaders') === 'true';
28
+ }
29
+
30
+ get uri() {
31
+ return this.#uri;
32
+ }
33
+
34
+ get loggerName() {
35
+ return this.#loggerName;
36
+ }
37
+
38
+ get level() {
39
+ return this.#level;
40
+ }
41
+
42
+ get showBody() {
43
+ return this.#showBody;
44
+ }
45
+
46
+ get showHeaders() {
47
+ return this.#showHeaders;
48
+ }
49
+
50
+ createProducer() {
51
+ return new LogProducer({
52
+ level: this.#level,
53
+ showBody: this.#showBody,
54
+ showHeaders: this.#showHeaders,
55
+ loggerName: this.#loggerName,
56
+ });
57
+ }
58
+
59
+ createConsumer() {
60
+ throw new Error('log: component is producer-only');
61
+ }
62
+ }
63
+
64
+ export { LogEndpoint };
65
+ export default LogEndpoint;
@@ -0,0 +1,58 @@
1
+ import { Producer } from '@alt-javascript/camel-lite-core';
2
+ import { LoggerFactory } from '@alt-javascript/logger';
3
+
4
+ const VALID_LEVELS = new Set(['info', 'warn', 'error', 'debug']);
5
+
6
+ class LogProducer extends Producer {
7
+ #level;
8
+ #showBody;
9
+ #showHeaders;
10
+ #loggerName;
11
+ #logger;
12
+
13
+ constructor({ level, showBody, showHeaders, loggerName } = {}) {
14
+ super();
15
+ this.#level = VALID_LEVELS.has(level) ? level : 'info';
16
+ this.#showBody = showBody !== false;
17
+ this.#showHeaders = showHeaders === true;
18
+ this.#loggerName = loggerName || 'log';
19
+ this.#logger = LoggerFactory.getLogger(this.#loggerName);
20
+ }
21
+
22
+ get level() {
23
+ return this.#level;
24
+ }
25
+
26
+ get showBody() {
27
+ return this.#showBody;
28
+ }
29
+
30
+ get showHeaders() {
31
+ return this.#showHeaders;
32
+ }
33
+
34
+ get loggerName() {
35
+ return this.#loggerName;
36
+ }
37
+
38
+ async send(exchange) {
39
+ const parts = [this.#loggerName];
40
+
41
+ if (this.#showBody) {
42
+ parts.push('body: ' + JSON.stringify(exchange.in.body));
43
+ }
44
+
45
+ if (this.#showHeaders && exchange.in.headers) {
46
+ const headers = exchange.in.headers instanceof Map
47
+ ? Object.fromEntries(exchange.in.headers)
48
+ : exchange.in.headers;
49
+ parts.push('headers: ' + JSON.stringify(headers));
50
+ }
51
+
52
+ const message = parts.join(' ');
53
+ this.#logger[this.#level](message);
54
+ }
55
+ }
56
+
57
+ export { LogProducer };
58
+ export default LogProducer;
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { LogComponent, default as LogComponentDefault } from './LogComponent.js';
2
+ export { LogEndpoint, default as LogEndpointDefault } from './LogEndpoint.js';
3
+ export { LogProducer, default as LogProducerDefault } from './LogProducer.js';
4
+
5
+ import LogComponent from './LogComponent.js';
6
+ export default LogComponent;
@@ -0,0 +1,80 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CamelContext, Exchange, RouteDefinition } from '@alt-javascript/camel-lite-core';
4
+ import { DirectComponent } from '@alt-javascript/camel-lite-component-direct';
5
+ import { LogComponent } from '@alt-javascript/camel-lite-component-log';
6
+
7
+ describe('End-to-end integration: direct: + log: components', () => {
8
+ it('routes an exchange through two chained direct: routes ending at log:', async () => {
9
+ const context = new CamelContext();
10
+ context.addComponent('direct', new DirectComponent());
11
+ context.addComponent('log', new LogComponent());
12
+
13
+ const routeA = new RouteDefinition('direct:entry');
14
+ routeA.process((exchange) => { exchange.in.body = 'hello'; });
15
+ routeA.to('direct:chain');
16
+
17
+ const routeB = new RouteDefinition('direct:chain');
18
+ routeB.process((exchange) => { exchange.in.body = exchange.in.body + ' world'; });
19
+ routeB.to('log:output?level=info&showBody=true');
20
+
21
+ context.addRoutes({ configure() {}, getRoutes() { return [routeA, routeB]; } });
22
+
23
+ await context.start();
24
+ try {
25
+ const exchange = new Exchange();
26
+ const entryConsumer = context.getConsumer('direct:entry');
27
+ assert.ok(entryConsumer, 'DirectConsumer for direct:entry must be registered after start()');
28
+
29
+ await entryConsumer.process(exchange);
30
+
31
+ assert.equal(exchange.in.body, 'hello world',
32
+ `Expected 'hello world', got: ${JSON.stringify(exchange.in.body)}`);
33
+ assert.equal(exchange.exception, null, 'exchange.exception should be null');
34
+ } finally {
35
+ await context.stop();
36
+ }
37
+ });
38
+
39
+ it('context.stop() deregisters all consumers', async () => {
40
+ const context = new CamelContext();
41
+ context.addComponent('direct', new DirectComponent());
42
+ context.addComponent('log', new LogComponent());
43
+
44
+ const routeA = new RouteDefinition('direct:entry2');
45
+ routeA.process((exchange) => { exchange.in.body = 'test'; });
46
+ routeA.to('log:sink?showBody=false');
47
+
48
+ context.addRoutes({ configure() {}, getRoutes() { return [routeA]; } });
49
+
50
+ await context.start();
51
+ assert.ok(context.getConsumer('direct:entry2'), 'consumer registered after start');
52
+
53
+ await context.stop();
54
+ const consumer = context.getConsumer('direct:entry2');
55
+ assert.ok(!consumer, 'consumer should be deregistered after stop');
56
+ });
57
+
58
+ it('standalone log producer route works end-to-end — exchange state correct', async () => {
59
+ const context = new CamelContext();
60
+ context.addComponent('direct', new DirectComponent());
61
+ context.addComponent('log', new LogComponent());
62
+
63
+ const route = new RouteDefinition('direct:standalone');
64
+ route.process((exchange) => { exchange.in.body = 'standalone message'; });
65
+ route.to('log:standalone?level=info&showBody=true');
66
+
67
+ context.addRoutes({ configure() {}, getRoutes() { return [route]; } });
68
+
69
+ await context.start();
70
+ try {
71
+ const exchange = new Exchange();
72
+ await context.getConsumer('direct:standalone').process(exchange);
73
+
74
+ assert.equal(exchange.in.body, 'standalone message');
75
+ assert.equal(exchange.exception, null);
76
+ } finally {
77
+ await context.stop();
78
+ }
79
+ });
80
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Exchange } from '@alt-javascript/camel-lite-core';
4
+ import { LogComponent, LogEndpoint, LogProducer } from '@alt-javascript/camel-lite-component-log';
5
+
6
+ describe('LogComponent', () => {
7
+ it('can be constructed', () => {
8
+ const lc = new LogComponent();
9
+ assert.ok(lc instanceof LogComponent);
10
+ });
11
+
12
+ it('createEndpoint returns LogEndpoint', () => {
13
+ const lc = new LogComponent();
14
+ const ep = lc.createEndpoint('log:myLogger', 'myLogger', {}, null);
15
+ assert.ok(ep instanceof LogEndpoint);
16
+ assert.equal(ep.uri, 'log:myLogger');
17
+ });
18
+ });
19
+
20
+ describe('LogEndpoint', () => {
21
+ it('createProducer returns LogProducer', () => {
22
+ const ep = new LogEndpoint('log:test');
23
+ const producer = ep.createProducer();
24
+ assert.ok(producer instanceof LogProducer);
25
+ });
26
+
27
+ it('createConsumer throws', () => {
28
+ const ep = new LogEndpoint('log:test');
29
+ assert.throws(() => ep.createConsumer(), { message: 'log: component is producer-only' });
30
+ });
31
+
32
+ it('parses URI: loggerName extracted correctly — defaults to info level', () => {
33
+ const ep = new LogEndpoint('log:myLogger');
34
+ assert.equal(ep.loggerName, 'myLogger');
35
+ assert.equal(ep.level, 'info');
36
+ assert.equal(ep.showBody, true);
37
+ assert.equal(ep.showHeaders, false);
38
+ });
39
+
40
+ it('parses URI: log:myLogger?level=info&showBody=false', () => {
41
+ const ep = new LogEndpoint('log:myLogger?level=info&showBody=false');
42
+ assert.equal(ep.loggerName, 'myLogger');
43
+ assert.equal(ep.level, 'info');
44
+ assert.equal(ep.showBody, false);
45
+ assert.equal(ep.showHeaders, false);
46
+ });
47
+
48
+ it('parses URI: log:output?level=warn&showBody=true&showHeaders=true', () => {
49
+ const ep = new LogEndpoint('log:output?level=warn&showBody=true&showHeaders=true');
50
+ assert.equal(ep.loggerName, 'output');
51
+ assert.equal(ep.level, 'warn');
52
+ assert.equal(ep.showBody, true);
53
+ assert.equal(ep.showHeaders, true);
54
+ });
55
+ });
56
+
57
+ describe('LogProducer', () => {
58
+ it('send() completes without error for a body exchange', async () => {
59
+ const producer = new LogProducer({ level: 'info', showBody: true, loggerName: 'test' });
60
+ const exchange = new Exchange();
61
+ exchange.in.body = { hello: 'world' };
62
+ await assert.doesNotReject(() => producer.send(exchange));
63
+ });
64
+
65
+ it('send() completes without error for debug level', async () => {
66
+ const producer = new LogProducer({ level: 'debug', showBody: true, loggerName: 'test' });
67
+ const exchange = new Exchange();
68
+ exchange.in.body = 'debug payload';
69
+ await assert.doesNotReject(() => producer.send(exchange));
70
+ });
71
+
72
+ it('send() completes without error when showBody=false', async () => {
73
+ const producer = new LogProducer({ level: 'info', showBody: false, loggerName: 'test' });
74
+ const exchange = new Exchange();
75
+ exchange.in.body = 'secret';
76
+ await assert.doesNotReject(() => producer.send(exchange));
77
+ });
78
+
79
+ it('send() completes without error when showHeaders=true', async () => {
80
+ const producer = new LogProducer({ level: 'info', showBody: false, showHeaders: true, loggerName: 'test' });
81
+ const exchange = new Exchange();
82
+ exchange.in.setHeader('x-trace', 'abc123');
83
+ await assert.doesNotReject(() => producer.send(exchange));
84
+ });
85
+
86
+ it('unknown level falls back to info', () => {
87
+ const producer = new LogProducer({ level: 'trace', showBody: true, loggerName: 'test' });
88
+ assert.equal(producer.level, 'info', 'unknown level should fall back to info');
89
+ });
90
+
91
+ it('log level alias falls back to info', () => {
92
+ const producer = new LogProducer({ level: 'log', showBody: true, loggerName: 'test' });
93
+ assert.equal(producer.level, 'info', 'log level should alias to info');
94
+ });
95
+
96
+ it('loggerName is used as the logger category', () => {
97
+ const producer = new LogProducer({ level: 'info', showBody: true, loggerName: 'myRoute' });
98
+ assert.equal(producer.loggerName, 'myRoute');
99
+ });
100
+
101
+ it('exchange is not mutated by send()', async () => {
102
+ const producer = new LogProducer({ level: 'info', showBody: true, loggerName: 'test' });
103
+ const exchange = new Exchange();
104
+ exchange.in.body = 'unchanged';
105
+ await producer.send(exchange);
106
+ assert.equal(exchange.in.body, 'unchanged');
107
+ assert.equal(exchange.exception, null);
108
+ });
109
+ });