Skip to content

Commit d986d3e

Browse files
Expand custom formats guide
1 parent 6998e2d commit d986d3e

1 file changed

Lines changed: 295 additions & 4 deletions

File tree

docs/guide/custom-formats.md

Lines changed: 295 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Custom Formats
22

3-
Add custom log format parsers via config files.
3+
loq supports adding custom log format parsers in two ways:
4+
1. **Config files** - Quick setup for personal/project use
5+
2. **Code contribution** - Add built-in parsers for everyone
46

5-
## Config File Locations
7+
## Config File Setup
8+
9+
### Config Locations
610

711
loq looks for config in these locations (in order):
812

@@ -14,7 +18,9 @@ loq looks for config in these locations (in order):
1418
6. `~/.loqrc.json`
1519
7. `~/.config/loq/config.json`
1620

17-
## TypeScript Config
21+
### TypeScript Config
22+
23+
Best for complex parsing logic:
1824

1925
```typescript
2026
// loq.config.ts
@@ -60,7 +66,9 @@ export default {
6066
};
6167
```
6268

63-
## JSON Config
69+
### JSON Config
70+
71+
Simpler option using regex patterns:
6472

6573
```json
6674
{
@@ -80,3 +88,286 @@ export default {
8088
]
8189
}
8290
```
91+
92+
## Format Definition
93+
94+
### Required Fields
95+
96+
| Field | Type | Description |
97+
|-------|------|-------------|
98+
| `name` | string | Unique identifier for the format |
99+
| `detect` | RegExp \| string \| function | How to identify this log format |
100+
| `parse` | object \| function | How to parse log lines |
101+
102+
### Detection Methods
103+
104+
**Regex pattern:**
105+
```typescript
106+
detect: /^\[\d{4}-\d{2}-\d{2}/
107+
```
108+
109+
**String (converted to regex):**
110+
```json
111+
"detect": "^\\[\\d{4}"
112+
```
113+
114+
**Function (most flexible):**
115+
```typescript
116+
detect: (line) => {
117+
try {
118+
const obj = JSON.parse(line);
119+
return 'my_special_field' in obj;
120+
} catch {
121+
return false;
122+
}
123+
}
124+
```
125+
126+
### Parse Methods
127+
128+
**Pattern-based:**
129+
```typescript
130+
parse: {
131+
pattern: /^\[(?<timestamp>[^\]]+)\] (?<level>\w+): (?<message>.+)$/,
132+
fields: {
133+
timestamp: 'timestamp', // named group
134+
level: 'level',
135+
message: 'message',
136+
},
137+
}
138+
```
139+
140+
**Function-based:**
141+
```typescript
142+
parse: (line) => {
143+
const parts = line.split(' | ');
144+
return {
145+
timestamp: parts[0],
146+
level: parts[1],
147+
message: parts[2],
148+
fields: {
149+
custom_field: parts[3],
150+
},
151+
};
152+
}
153+
```
154+
155+
### LogEntry Structure
156+
157+
Your parser should return this structure:
158+
159+
```typescript
160+
interface LogEntry {
161+
raw: string; // Original line (added automatically)
162+
timestamp?: string; // ISO timestamp or parseable date
163+
level?: string; // error, warn, info, debug, etc.
164+
message?: string; // Main log message
165+
fields?: Record<string, unknown>; // Additional fields
166+
}
167+
```
168+
169+
## Using Custom Formats
170+
171+
Once configured, loq auto-detects your format:
172+
173+
```bash
174+
# Auto-detect
175+
loq app.log
176+
177+
# Force specific format
178+
loq app.log --format my-app
179+
180+
# Query custom fields
181+
loq app.log where custom_field=value
182+
```
183+
184+
## Command Aliases
185+
186+
Define shortcuts for common queries:
187+
188+
```typescript
189+
aliases: {
190+
errors: 'where level=error',
191+
slow: 'where response_time>1000',
192+
today: 'where timestamp after today',
193+
'5xx': 'where status>=500 and status<600',
194+
}
195+
```
196+
197+
Usage:
198+
```bash
199+
loq app.log errors # expands to: where level=error
200+
loq access.log slow # expands to: where response_time>1000
201+
loq app.log errors limit 10 # can combine with other options
202+
```
203+
204+
## Examples
205+
206+
### Rails Log Format
207+
208+
```typescript
209+
{
210+
name: 'rails',
211+
detect: /^[DIWEF],\s*\[/,
212+
parse: {
213+
pattern: /^(?<level>[DIWEF]),\s*\[(?<timestamp>[^\]]+)\]\s*(?<pid>\d+)\s*(?<source>\w+)\s*--\s*:\s*(?<message>.*)$/,
214+
fields: {
215+
level: 'level',
216+
timestamp: 'timestamp',
217+
message: 'message',
218+
pid: 'pid',
219+
source: 'source',
220+
},
221+
},
222+
}
223+
```
224+
225+
### Docker JSON Logs
226+
227+
```typescript
228+
{
229+
name: 'docker-json',
230+
detect: (line) => {
231+
try {
232+
const obj = JSON.parse(line);
233+
return 'log' in obj && 'stream' in obj;
234+
} catch {
235+
return false;
236+
}
237+
},
238+
parse: (line) => {
239+
const obj = JSON.parse(line);
240+
return {
241+
timestamp: obj.time,
242+
level: obj.stream === 'stderr' ? 'error' : 'info',
243+
message: obj.log.trim(),
244+
fields: obj,
245+
};
246+
},
247+
}
248+
```
249+
250+
### Custom App with Metadata
251+
252+
```typescript
253+
{
254+
name: 'my-service',
255+
detect: /^\d{4}-\d{2}-\d{2}T.*\|/,
256+
parse: (line) => {
257+
const [timestamp, level, service, traceId, ...rest] = line.split(' | ');
258+
return {
259+
timestamp,
260+
level: level.toLowerCase(),
261+
message: rest.join(' | '),
262+
fields: {
263+
service,
264+
traceId,
265+
},
266+
};
267+
},
268+
}
269+
```
270+
271+
## Contributing Built-in Parsers
272+
273+
Want to add a parser for everyone? Follow these steps:
274+
275+
### 1. Create Parser File
276+
277+
```typescript
278+
// src/parser/formats/myformat.ts
279+
import type { LogEntry, LogParser } from '../types';
280+
281+
export const myFormatParser: LogParser = {
282+
name: 'myformat',
283+
284+
detect(line: string): boolean {
285+
return line.startsWith('MYFORMAT:');
286+
},
287+
288+
parse(line: string): LogEntry | null {
289+
const match = line.match(/^MYFORMAT: \[(.+?)\] (\w+) - (.+)$/);
290+
if (!match) return null;
291+
292+
return {
293+
raw: line,
294+
timestamp: match[1],
295+
level: match[2],
296+
message: match[3],
297+
fields: {},
298+
};
299+
},
300+
};
301+
```
302+
303+
### 2. Register Parser
304+
305+
```typescript
306+
// src/parser/auto-detect.ts
307+
import { myFormatParser } from './formats/myformat';
308+
309+
const builtinParsers: LogParser[] = [
310+
jsonParser,
311+
apacheParser,
312+
syslogParser,
313+
clfParser,
314+
myFormatParser, // Add here
315+
];
316+
```
317+
318+
### 3. Add Tests
319+
320+
```typescript
321+
// tests/parser/myformat.test.ts
322+
import { describe, expect, test } from 'bun:test';
323+
import { myFormatParser } from '../../src/parser/formats/myformat';
324+
325+
describe('myformat parser', () => {
326+
test('detects myformat lines', () => {
327+
expect(myFormatParser.detect('MYFORMAT: [2024-01-01] INFO - Hello')).toBe(true);
328+
expect(myFormatParser.detect('Some other log')).toBe(false);
329+
});
330+
331+
test('parses myformat correctly', () => {
332+
const result = myFormatParser.parse('MYFORMAT: [2024-01-01] INFO - Hello world');
333+
expect(result).toEqual({
334+
raw: 'MYFORMAT: [2024-01-01] INFO - Hello world',
335+
timestamp: '2024-01-01',
336+
level: 'INFO',
337+
message: 'Hello world',
338+
fields: {},
339+
});
340+
});
341+
342+
test('returns null for invalid lines', () => {
343+
expect(myFormatParser.parse('invalid line')).toBeNull();
344+
});
345+
});
346+
```
347+
348+
### 4. Submit PR
349+
350+
1. Fork the repo
351+
2. Create a branch: `git checkout -b add-myformat-parser`
352+
3. Make your changes
353+
4. Run tests: `bun test`
354+
5. Push and open a PR
355+
356+
## Debugging Custom Formats
357+
358+
Test your format detection:
359+
360+
```bash
361+
# See which format is detected
362+
loq app.log --format auto --verbose
363+
364+
# Force your format to test parsing
365+
loq app.log --format my-custom-format
366+
```
367+
368+
Check if fields are parsed correctly:
369+
370+
```bash
371+
# Output as JSON to see all fields
372+
loq app.log -o json limit 1
373+
```

0 commit comments

Comments
 (0)