Skip to content

Commit 05ab373

Browse files
committed
feat: replace Unicode delimiters with newline-based metadata separation
1 parent a082a75 commit 05ab373

3 files changed

Lines changed: 44 additions & 47 deletions

File tree

README.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,24 @@ npm install strog
2020

2121
## How It Works
2222

23-
Strog appends structured metadata to your log messages using invisible Unicode characters as delimiters. This approach keeps logs human-readable while making them machine-parsable.
23+
Strog appends structured metadata to your log messages on a separate line, making logs both human-readable and machine-parsable.
2424

25-
### The Magic: Unicode Delimiters
25+
### The Magic: Newline Separation
2626

27-
When you create a structured log, Strog appends metadata after your message using special Unicode characters:
27+
When you create a structured log, Strog appends metadata after your message on a new line:
2828

2929
```typescript
3030
const EndpointMetric = StructuredTag('endpoint-metric', ['method', 'endpoint'], false);
3131
const log = EndpointMetric`GET /users`;
3232

33-
// What you see: "GET /users"
34-
// What's actually there: "GET /users\u2009{\"type\":\"endpoint-metric\",\"metadata\":{\"method\":\"GET\",\"endpoint\":\"/users\"}}"
33+
// What you see:
34+
// GET /users
35+
// {"type":"endpoint-metric","metadata":{"method":"GET","endpoint":"/users"}}
3536
```
3637

3738
**Two modes:**
38-
- **Development** (`encode: false`): Uses **Thin Space** (`\u2009`) - metadata is visible in console output
39-
- **Production** (`encode: true`): Uses **Zero-Width Space** (`\u200B`) - metadata is hidden from console output, base64 encoded and still available to log processors (tails)
39+
- **Development** (`encode: false`): Metadata appears as readable JSON on the next line
40+
- **Production** (`encode: true`): Metadata is base64 encoded and prefixed with zero-width space (`\u200B`) for identification
4041

4142
### Log Processing & Tail Workers
4243

@@ -47,7 +48,7 @@ The same library can be used in log processors to extract the structured data:
4748
import { parseStructured, parseMeta } from 'strog';
4849

4950
// Parse a log message that came through your system
50-
const incomingLog = "GET /users completed in 150ms\u2009{...metadata...}";
51+
const incomingLog = "GET /users completed in 150ms\n{...metadata...}";
5152

5253
const parsed = parseStructured(incomingLog);
5354
console.log(parsed.raw); // "GET /users completed in 150ms" (clean message)
@@ -89,13 +90,14 @@ const logMessage = EndpointMetric`${method} ${endpoint} time: ${time_ms}ms code:
8990
console.info(logMessage);
9091

9192
/* Output in development:
92-
* GET /users time: 150ms code: 200\u2009{"method":"get","endpoint":"/users","time_ms":150,"status_code":200}
93+
* GET /users time: 150ms code: 200
94+
* {"type":"endpoint-metric","metadata":{"method":"GET","endpoint":"/users","time_ms":150,"status_code":200}}
9395
*/
9496

9597
/* Output in production:
96-
* GET /users/123 time: 150ms code: 200\u200B[hidden-base64-json-metadata]
97-
* or more simply:
98-
* GET /users/123 time: 150ms code: 200
98+
* GET /users time: 150ms code: 200
99+
* [zero-width-space]eyJhYmMiOi4uLn0= (base64 encoded metadata)
100+
*/
99101
```
100102

101103
## API Reference
@@ -196,8 +198,8 @@ import { StructuredTag } from 'strog';
196198

197199
const isProduction = process.env.NODE_ENV === 'production';
198200

199-
// In development: metadata visible as JSON
200-
// In production: metadata base64 encoded and hidden
201+
// In development: metadata visible as JSON on separate line
202+
// In production: metadata base64 encoded on separate line
201203
const ApiCall = StructuredTag('api-call', ['method', 'url', 'status'], isProduction);
202204

203205
const logMessage = ApiCall`${method} ${url} returned ${status}`;
@@ -206,12 +208,12 @@ console.info(logMessage);
206208

207209
## Metadata Encoding
208210

209-
Strog uses Unicode characters to separate log messages from metadata:
211+
Strog separates log messages from metadata using newlines:
210212

211-
- **Thin Space (\\u2009)**: Visible metadata for development
212-
- **Zero-Width Space (\\u200B)**: Hidden, base64-encoded metadata for production
213+
- **Development mode**: Metadata appears as readable JSON on the next line
214+
- **Production mode**: Metadata is base64 encoded and prefixed with zero-width space (`\u200B`) for identification
213215

214-
This approach ensures logs remain readable while enabling automated parsing.
216+
This approach ensures logs remain readable while enabling automated parsing by log processors.
215217

216218
## Framework Integration
217219

src/index.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function buildParsedMetadata(type: string, placeholders: any[], keys: str
2929
export function buildStringifiedMetadata(type: string, placeholders: any[], keys: string[], encode = false): string {
3030
const parsed = buildParsedMetadata(type, placeholders, keys);
3131
const stringified = JSON.stringify(parsed);
32-
return encode ? `\u200B${btoa(stringified)}` : `\u2009${stringified}`;
32+
return encode ? `\n\u200B${btoa(stringified)}` : `\n${stringified}`;
3333
}
3434

3535
export function StructuredTag(type: string, keys: string[], encode = false) {
@@ -54,23 +54,18 @@ export function safeJsonParse<T>(json: string): T | void {
5454

5555
export function parseStructured(structuredLogMessage: string): ParsedStructuredLog {
5656
let decoded: string | undefined;
57-
let raw: string;
58-
59-
if (structuredLogMessage.includes('\u200B')) {
60-
const parts = structuredLogMessage.split('\u200B');
61-
raw = parts[0] || '';
62-
const encoded = parts[1];
63-
try {
64-
decoded = encoded ? atob(encoded) : undefined;
65-
} catch {
66-
decoded = undefined;
67-
}
68-
} else if (structuredLogMessage.includes('\u2009')) {
69-
const parts = structuredLogMessage.split('\u2009');
70-
raw = parts[0] || '';
71-
decoded = parts[1];
72-
} else {
73-
raw = structuredLogMessage;
57+
//let raw: string;
58+
59+
const [ raw, encoded ] = structuredLogMessage.split('\n');
60+
61+
if ( encoded ) {
62+
if ( encoded.startsWith('\u200B') ) {
63+
try {
64+
decoded = atob ( encoded.substring(1) );
65+
} catch {
66+
decoded = undefined;
67+
}
68+
} else decoded = encoded;
7469
}
7570

7671
const parsed = decoded ? safeJsonParse<ParsedMetadata>(decoded) : undefined;

tests/index.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,18 @@ describe('buildParsedMetadata', () => {
7474
});
7575

7676
describe('buildStringifiedMetadata', () => {
77-
test('should stringify metadata with thin space', () => {
77+
test('should stringify metadata with newline', () => {
7878
const stringified = buildStringifiedMetadata('test-type', ['value1', 42], ['key1', 'key2'], false);
7979
const expectedJson = JSON.stringify({ type: 'test-type', metadata: { key1: 'value1', key2: 42 } });
8080

81-
assert.equal(stringified, `\u2009${expectedJson}`);
81+
assert.equal(stringified, `\n${expectedJson}`);
8282
});
8383

8484
test('should stringify metadata with encoding', () => {
8585
const stringified = buildStringifiedMetadata('test-type', ['value1'], ['key1'], true);
8686
const expectedEncoded = btoa(JSON.stringify({ type: 'test-type', metadata: { key1: 'value1' } }));
8787

88-
assert.equal(stringified, `\u200B${expectedEncoded}`);
88+
assert.equal(stringified, `\n\u200B${expectedEncoded}`);
8989
});
9090
});
9191

@@ -101,15 +101,15 @@ describe('StructuredTag', () => {
101101
const expectedBase = 'GET /users/123 time: 150ms code: 200';
102102

103103
assert.ok(logMessage.startsWith(expectedBase));
104-
assert.ok(logMessage.includes('\u2009'));
104+
assert.ok(logMessage.includes('\n'));
105105
});
106106

107107
test('should work with encoding', () => {
108108
const EncodedMetric = StructuredTag('encoded-metric', ['key'], true);
109109
const encodedMessage = EncodedMetric`Test: ${'test-value'}`;
110110

111111
assert.ok(encodedMessage.startsWith('Test: test-value'));
112-
assert.ok(encodedMessage.includes('\u200B'));
112+
assert.ok(encodedMessage.includes('\n\u200B'));
113113
});
114114
});
115115

@@ -130,8 +130,8 @@ describe('safeJsonParse', () => {
130130
});
131131

132132
describe('parseStructured', () => {
133-
test('should extract raw message and parse metadata from thin space format', () => {
134-
const testMessage = 'Hello world\u2009{"type":"test","metadata":{"key":"value"}}';
133+
test('should extract raw message and parse metadata from newline format', () => {
134+
const testMessage = 'Hello world\n{"type":"test","metadata":{"key":"value"}}';
135135
const parsed = parseStructured(testMessage);
136136

137137
assert.equal(parsed.raw, 'Hello world');
@@ -141,7 +141,7 @@ describe('parseStructured', () => {
141141
test('should extract raw message and decode metadata from encoded format', () => {
142142
const testMetadata = { type: 'test', metadata: { key: 'encoded' } };
143143
const encoded = btoa(JSON.stringify(testMetadata));
144-
const testMessage = `Hello encoded\u200B${encoded}`;
144+
const testMessage = `Hello encoded\n\u200B${encoded}`;
145145
const parsed = parseStructured(testMessage);
146146

147147
assert.equal(parsed.raw, 'Hello encoded');
@@ -157,7 +157,7 @@ describe('parseStructured', () => {
157157
});
158158

159159
test('should handle malformed encoded data gracefully', () => {
160-
const malformedEncoded = 'Test\u200Binvalid-base64!';
160+
const malformedEncoded = 'Test\n\u200Binvalid-base64!';
161161
const parsed = parseStructured(malformedEncoded);
162162

163163
assert.equal(parsed.raw, 'Test');
@@ -167,7 +167,7 @@ describe('parseStructured', () => {
167167

168168
describe('parseMeta', () => {
169169
test('should extract metadata', () => {
170-
const testMessage = 'Hello world\u2009{"type":"test","metadata":{"key":"value"}}';
170+
const testMessage = 'Hello world\n{"type":"test","metadata":{"key":"value"}}';
171171
const meta = parseMeta(testMessage);
172172

173173
assert.deepEqual(meta, { type: 'test', metadata: { key: 'value' } });
@@ -183,7 +183,7 @@ describe('parseMeta', () => {
183183

184184
describe('extractLog', () => {
185185
test('should extract raw message', () => {
186-
const testMessage = 'Hello world\u2009{"type":"test","metadata":{"key":"value"}}';
186+
const testMessage = 'Hello world\n{"type":"test","metadata":{"key":"value"}}';
187187
const log = extractLog(testMessage);
188188

189189
assert.equal(log, 'Hello world');

0 commit comments

Comments
 (0)