Skip to content

Commit 56ce9c2

Browse files
authored
Merge pull request #73 from mitre-attack/feat/enable-console-log-controls
feat(logger): add configurable logger to control console output
2 parents 33f2878 + f1627d6 commit 56ce9c2

7 files changed

Lines changed: 274 additions & 7 deletions

File tree

USAGE.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,61 @@ import { registerDataSource, loadDataModel, DataSource } from '@mitre-attack/att
326326
- **Strict Mode**: Data must pass all validation checks to be ingested. If any objects fail validation, the registration is aborted.
327327
- **Relaxed Mode**: Invalid objects are logged, but the library attempts to load the dataset anyway. Use with caution, as this may cause unexpected errors during usage.
328328

329+
## Logging
330+
331+
The library emits log output during data source registration, bundle parsing, and refinement checks. By default, only `warn` and `error` messages are written to the console — informational status messages (e.g. "Retrieved data", "Parsed data") are suppressed.
332+
333+
### Log Levels
334+
335+
| Level | Description |
336+
|----------|--------------------------------------------------------------------|
337+
| `debug` | Verbose diagnostic output. |
338+
| `info` | Informational status messages (data retrieval, parse counts, etc). |
339+
| `warn` | Validation issues in `relaxed` mode and deprecation warnings. |
340+
| `error` | Errors only. |
341+
| `silent` | Disables all output. |
342+
343+
Levels are inclusive: setting `info` enables `info`, `warn`, and `error`. The default is `warn`.
344+
345+
### Configuring the Logger
346+
347+
Use `configureLogger` to set the level or replace the output handler:
348+
349+
```typescript
350+
import { configureLogger } from '@mitre-attack/attack-data-model';
351+
352+
// Silence all library output (useful when parsing many bundles in a row)
353+
configureLogger({ level: 'silent' });
354+
355+
// Or surface informational messages
356+
configureLogger({ level: 'info' });
357+
```
358+
359+
You can also set the level via the `ADM_LOG_LEVEL` environment variable:
360+
361+
```bash
362+
ADM_LOG_LEVEL=silent node ./my-script.js
363+
```
364+
365+
An explicit `configureLogger({ level })` call always takes precedence over the environment variable.
366+
367+
### Custom Log Handlers
368+
369+
Provide your own handler to route log output to a logging library or external system instead of the console:
370+
371+
```typescript
372+
import { configureLogger } from '@mitre-attack/attack-data-model';
373+
import type { LogHandler } from '@mitre-attack/attack-data-model';
374+
375+
const handler: LogHandler = (level, message) => {
376+
myLogger.log({ level, message, source: 'attack-data-model' });
377+
};
378+
379+
configureLogger({ level: 'info', handler });
380+
```
381+
382+
Call `resetLogger()` to restore the default level and handler.
383+
329384
## Examples
330385

331386
### Accessing Techniques and Related Tactics
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import WorkInProgressNotice from '@site/src/components/WorkInProgressNotice';
2+
3+
# How to Configure Logging
4+
5+
<WorkInProgressNotice />
6+
7+
**Control console output from the ATT&CK Data Model**
8+
9+
The library emits log output during data source registration, bundle parsing, and refinement checks. This guide shows you how to silence that output, surface more diagnostic detail, or route messages to your own logger.
10+
11+
## Problem
12+
13+
Use this guide when you need to:
14+
15+
- Silence library output entirely (e.g. when parsing many bundles in a loop and the noise becomes overwhelming)
16+
- See additional informational messages while debugging a data load
17+
- Forward log messages to a structured logger like `pino`, `winston`, or `bunyan`
18+
- Configure log behavior via an environment variable for different deployment environments
19+
20+
## Default Behavior
21+
22+
By default, the library logs at the `warn` level. This means:
23+
24+
- `warn` and `error` messages are printed to the console
25+
- `info` messages (e.g. "Retrieved data", "Parsed data") are suppressed
26+
- `debug` messages are suppressed
27+
28+
The default handler routes output to `console.log` (`debug`/`info`), `console.warn`, and `console.error`.
29+
30+
## Log Levels
31+
32+
| Level | Description |
33+
|----------|--------------------------------------------------------------------|
34+
| `debug` | Verbose diagnostic output. |
35+
| `info` | Informational status messages (data retrieval, parse counts, etc). |
36+
| `warn` | Validation issues in `relaxed` mode and deprecation warnings. |
37+
| `error` | Errors only. |
38+
| `silent` | Disables all output. |
39+
40+
Levels are inclusive: setting the level to `info` enables `info`, `warn`, and `error` messages.
41+
42+
## Solution 1: Silence All Output
43+
44+
When parsing large bundles or iterating over relationships in a tight loop, the deprecation warnings and validation messages can dominate stdout. Silence them with `configureLogger`:
45+
46+
```typescript
47+
import { configureLogger } from '@mitre-attack/attack-data-model';
48+
49+
configureLogger({ level: 'silent' });
50+
```
51+
52+
## Solution 2: Surface Informational Output
53+
54+
To see status messages emitted during data source registration:
55+
56+
```typescript
57+
import { configureLogger } from '@mitre-attack/attack-data-model';
58+
59+
configureLogger({ level: 'info' });
60+
```
61+
62+
## Solution 3: Configure via Environment Variable
63+
64+
Set `ADM_LOG_LEVEL` to any valid level (`debug`, `info`, `warn`, `error`, `silent`):
65+
66+
```bash
67+
ADM_LOG_LEVEL=silent node ./my-script.js
68+
```
69+
70+
This is useful when you want different log behavior in CI versus local development without changing code. An explicit `configureLogger({ level })` call always wins over the environment variable.
71+
72+
## Solution 4: Provide a Custom Handler
73+
74+
To integrate with a structured logger, supply a `LogHandler`:
75+
76+
```typescript
77+
import { configureLogger } from '@mitre-attack/attack-data-model';
78+
import type { LogHandler } from '@mitre-attack/attack-data-model';
79+
import pino from 'pino';
80+
81+
const log = pino();
82+
83+
const handler: LogHandler = (level, message) => {
84+
log[level]({ source: 'attack-data-model' }, message);
85+
};
86+
87+
configureLogger({ level: 'info', handler });
88+
```
89+
90+
The handler receives the level (`debug`, `info`, `warn`, or `error` — never `silent`) and the message string. Configure the level and handler independently, or together in a single call.
91+
92+
## Solution 5: Reset to Defaults
93+
94+
To restore the default level and handler — useful in test suites that mutate logger state:
95+
96+
```typescript
97+
import { resetLogger } from '@mitre-attack/attack-data-model';
98+
99+
afterEach(() => {
100+
resetLogger();
101+
});
102+
```
103+
104+
## Reference
105+
106+
```typescript
107+
import {
108+
configureLogger,
109+
resetLogger,
110+
} from '@mitre-attack/attack-data-model';
111+
import type {
112+
LogLevel,
113+
LogHandler,
114+
LoggerConfig,
115+
} from '@mitre-attack/attack-data-model';
116+
```
117+
118+
- `configureLogger(config: LoggerConfig)`: Apply the supplied `level` and/or `handler`. Either field is optional — provide only what you want to change.
119+
- `resetLogger()`: Clear any overrides; subsequent calls fall back to the `ADM_LOG_LEVEL` environment variable, or to `warn` if it is unset.
120+
121+
---

docusaurus/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const sidebars: SidebarsConfig = {
2828
'how-to-guides/manage-data-sources',
2929
'how-to-guides/validate-bundles',
3030
'how-to-guides/schema-variants',
31+
'how-to-guides/configure-logging',
3132
'how-to-guides/error-handling',
3233
'how-to-guides/performance',
3334
],

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export { ATTACK_SPEC_VERSION } from '@/attack-spec-version.js';
22
export * from '@/classes/index.js';
33
export * from '@/data-sources/index.js';
4+
export { configureLogger, resetLogger } from '@/logger.js';
5+
export type { LogLevel, LogHandler, LoggerConfig } from '@/logger.js';
46
export * from '@/main.js';
57
export * from '@/refinements/index.js';
68
export * from '@/schemas/index.js';

src/logger.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
2+
export type LogHandler = (level: Exclude<LogLevel, 'silent'>, message: string) => void;
3+
4+
export interface LoggerConfig {
5+
level?: LogLevel;
6+
handler?: LogHandler;
7+
}
8+
9+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
10+
debug: 0,
11+
info: 1,
12+
warn: 2,
13+
error: 3,
14+
silent: 4,
15+
};
16+
17+
const VALID_LOG_LEVELS = new Set<string>(Object.keys(LOG_LEVEL_PRIORITY));
18+
19+
function getDefaultLevel(): LogLevel {
20+
if (typeof process !== 'undefined' && process.env?.ADM_LOG_LEVEL) {
21+
const envLevel = process.env.ADM_LOG_LEVEL.toLowerCase();
22+
if (VALID_LOG_LEVELS.has(envLevel)) {
23+
return envLevel as LogLevel;
24+
}
25+
}
26+
return 'warn';
27+
}
28+
29+
const defaultHandler: LogHandler = (level, message) => {
30+
switch (level) {
31+
case 'debug':
32+
case 'info':
33+
console.log(message);
34+
break;
35+
case 'warn':
36+
console.warn(message);
37+
break;
38+
case 'error':
39+
console.error(message);
40+
break;
41+
}
42+
};
43+
44+
let currentLevel: LogLevel | undefined;
45+
let currentHandler: LogHandler | undefined;
46+
47+
function getLevel(): LogLevel {
48+
return currentLevel ?? getDefaultLevel();
49+
}
50+
51+
function getHandler(): LogHandler {
52+
return currentHandler ?? defaultHandler;
53+
}
54+
55+
function shouldLog(level: Exclude<LogLevel, 'silent'>): boolean {
56+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[getLevel()];
57+
}
58+
59+
export function configureLogger(config: LoggerConfig): void {
60+
if (config.level !== undefined) {
61+
currentLevel = config.level;
62+
}
63+
if (config.handler !== undefined) {
64+
currentHandler = config.handler;
65+
}
66+
}
67+
68+
export function resetLogger(): void {
69+
currentLevel = undefined;
70+
currentHandler = undefined;
71+
}
72+
73+
export const logger = {
74+
debug(message: string): void {
75+
if (shouldLog('debug')) getHandler()('debug', message);
76+
},
77+
info(message: string): void {
78+
if (shouldLog('info')) getHandler()('info', message);
79+
},
80+
warn(message: string): void {
81+
if (shouldLog('warn')) getHandler()('warn', message);
82+
},
83+
error(message: string): void {
84+
if (shouldLog('error')) getHandler()('error', message);
85+
},
86+
};

src/main.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios from 'axios';
22
import { v4 as uuidv4 } from 'uuid';
3+
import { logger } from '@/logger.js';
34

45
import {
56
stixBundleSchema,
@@ -103,14 +104,14 @@ export async function registerDataSource(registration: DataSourceRegistration):
103104
throw new Error(`Unsupported source type: ${source}`);
104105
}
105106

106-
console.log('Retrieved data');
107+
logger.info('Retrieved data');
107108

108109
const parsedAttackObjects = parseStixBundle(rawData, parsingMode);
109-
console.log('Parsed data.');
110-
console.log(parsedAttackObjects.length);
110+
logger.info('Parsed data.');
111+
logger.info(`${parsedAttackObjects.length}`);
111112

112113
const model = new AttackDataModel(uniqueId, parsedAttackObjects);
113-
console.log('Initialized data model.');
114+
logger.info('Initialized data model.');
114115

115116
// Store the model and its unique ID in the dataSources map
116117
dataSources[uniqueId] = { id: uniqueId, model };
@@ -217,7 +218,7 @@ function parseStixBundle(rawData: StixBundle, parsingMode: ParsingMode): AttackO
217218
if (parsingMode === 'strict') {
218219
throw new Error(`Bundle validation failed:\n${errors.join('\n')}`);
219220
} else {
220-
console.warn(`Bundle validation errors:\n${errors.join('\n')}`);
221+
logger.warn(`Bundle validation errors:\n${errors.join('\n')}`);
221222
}
222223
return []; // Return empty array if bundle itself is invalid
223224
}
@@ -310,7 +311,7 @@ function parseStixBundle(rawData: StixBundle, parsingMode: ParsingMode): AttackO
310311
if (parsingMode === 'strict') {
311312
throw new Error(`Validation errors:\n${errors.join('\n')}`);
312313
} else {
313-
console.warn(`Validation errors:\n${errors.join('\n')}`);
314+
logger.warn(`Validation errors:\n${errors.join('\n')}`);
314315
}
315316
}
316317

src/schemas/sro/relationship.schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod/v4';
2+
import { logger } from '@/logger.js';
23
import { attackBaseRelationshipObjectSchema } from '../common/index.js';
34
import {
45
createStixIdValidator,
@@ -324,7 +325,7 @@ export const relationshipChecks = (ctx: z.core.ParsePayload<RelationshipPartial>
324325
ctx.value.relationship_type === 'detects' &&
325326
ctx.value.target_ref.startsWith('attack-pattern--')
326327
) {
327-
console.warn(
328+
logger.warn(
328329
'DEPRECATION WARNING: x-mitre-data-component -> detects -> attack-pattern relationships are deprecated',
329330
);
330331
}

0 commit comments

Comments
 (0)