Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,61 @@ import { registerDataSource, loadDataModel, DataSource } from '@mitre-attack/att
- **Strict Mode**: Data must pass all validation checks to be ingested. If any objects fail validation, the registration is aborted.
- **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.

## Logging

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.

### Log Levels

| Level | Description |
|----------|--------------------------------------------------------------------|
| `debug` | Verbose diagnostic output. |
| `info` | Informational status messages (data retrieval, parse counts, etc). |
| `warn` | Validation issues in `relaxed` mode and deprecation warnings. |
| `error` | Errors only. |
| `silent` | Disables all output. |

Levels are inclusive: setting `info` enables `info`, `warn`, and `error`. The default is `warn`.

### Configuring the Logger

Use `configureLogger` to set the level or replace the output handler:

```typescript
import { configureLogger } from '@mitre-attack/attack-data-model';

// Silence all library output (useful when parsing many bundles in a row)
configureLogger({ level: 'silent' });

// Or surface informational messages
configureLogger({ level: 'info' });
```

You can also set the level via the `ADM_LOG_LEVEL` environment variable:

```bash
ADM_LOG_LEVEL=silent node ./my-script.js
```

An explicit `configureLogger({ level })` call always takes precedence over the environment variable.

### Custom Log Handlers

Provide your own handler to route log output to a logging library or external system instead of the console:

```typescript
import { configureLogger } from '@mitre-attack/attack-data-model';
import type { LogHandler } from '@mitre-attack/attack-data-model';

const handler: LogHandler = (level, message) => {
myLogger.log({ level, message, source: 'attack-data-model' });
};

configureLogger({ level: 'info', handler });
```

Call `resetLogger()` to restore the default level and handler.

## Examples

### Accessing Techniques and Related Tactics
Expand Down
121 changes: 121 additions & 0 deletions docusaurus/docs/how-to-guides/configure-logging.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import WorkInProgressNotice from '@site/src/components/WorkInProgressNotice';

# How to Configure Logging

<WorkInProgressNotice />

**Control console output from the ATT&CK Data Model**

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.

## Problem

Use this guide when you need to:

- Silence library output entirely (e.g. when parsing many bundles in a loop and the noise becomes overwhelming)
- See additional informational messages while debugging a data load
- Forward log messages to a structured logger like `pino`, `winston`, or `bunyan`
- Configure log behavior via an environment variable for different deployment environments

## Default Behavior

By default, the library logs at the `warn` level. This means:

- `warn` and `error` messages are printed to the console
- `info` messages (e.g. "Retrieved data", "Parsed data") are suppressed
- `debug` messages are suppressed

The default handler routes output to `console.log` (`debug`/`info`), `console.warn`, and `console.error`.

## Log Levels

| Level | Description |
|----------|--------------------------------------------------------------------|
| `debug` | Verbose diagnostic output. |
| `info` | Informational status messages (data retrieval, parse counts, etc). |
| `warn` | Validation issues in `relaxed` mode and deprecation warnings. |
| `error` | Errors only. |
| `silent` | Disables all output. |

Levels are inclusive: setting the level to `info` enables `info`, `warn`, and `error` messages.

## Solution 1: Silence All Output

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`:

```typescript
import { configureLogger } from '@mitre-attack/attack-data-model';

configureLogger({ level: 'silent' });
```

## Solution 2: Surface Informational Output

To see status messages emitted during data source registration:

```typescript
import { configureLogger } from '@mitre-attack/attack-data-model';

configureLogger({ level: 'info' });
```

## Solution 3: Configure via Environment Variable

Set `ADM_LOG_LEVEL` to any valid level (`debug`, `info`, `warn`, `error`, `silent`):

```bash
ADM_LOG_LEVEL=silent node ./my-script.js
```

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.

## Solution 4: Provide a Custom Handler

To integrate with a structured logger, supply a `LogHandler`:

```typescript
import { configureLogger } from '@mitre-attack/attack-data-model';
import type { LogHandler } from '@mitre-attack/attack-data-model';
import pino from 'pino';

const log = pino();

const handler: LogHandler = (level, message) => {
log[level]({ source: 'attack-data-model' }, message);
};

configureLogger({ level: 'info', handler });
```

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.

## Solution 5: Reset to Defaults

To restore the default level and handler — useful in test suites that mutate logger state:

```typescript
import { resetLogger } from '@mitre-attack/attack-data-model';

afterEach(() => {
resetLogger();
});
```

## Reference

```typescript
import {
configureLogger,
resetLogger,
} from '@mitre-attack/attack-data-model';
import type {
LogLevel,
LogHandler,
LoggerConfig,
} from '@mitre-attack/attack-data-model';
```

- `configureLogger(config: LoggerConfig)`: Apply the supplied `level` and/or `handler`. Either field is optional — provide only what you want to change.
- `resetLogger()`: Clear any overrides; subsequent calls fall back to the `ADM_LOG_LEVEL` environment variable, or to `warn` if it is unset.

---
1 change: 1 addition & 0 deletions docusaurus/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const sidebars: SidebarsConfig = {
'how-to-guides/manage-data-sources',
'how-to-guides/validate-bundles',
'how-to-guides/schema-variants',
'how-to-guides/configure-logging',
'how-to-guides/error-handling',
'how-to-guides/performance',
],
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { ATTACK_SPEC_VERSION } from '@/attack-spec-version.js';
export * from '@/classes/index.js';
export * from '@/data-sources/index.js';
export { configureLogger, resetLogger } from '@/logger.js';
export type { LogLevel, LogHandler, LoggerConfig } from '@/logger.js';
export * from '@/main.js';
export * from '@/refinements/index.js';
export * from '@/schemas/index.js';
86 changes: 86 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
export type LogHandler = (level: Exclude<LogLevel, 'silent'>, message: string) => void;

export interface LoggerConfig {
level?: LogLevel;
handler?: LogHandler;
}

const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
silent: 4,
};

const VALID_LOG_LEVELS = new Set<string>(Object.keys(LOG_LEVEL_PRIORITY));

function getDefaultLevel(): LogLevel {
if (typeof process !== 'undefined' && process.env?.ADM_LOG_LEVEL) {
const envLevel = process.env.ADM_LOG_LEVEL.toLowerCase();
if (VALID_LOG_LEVELS.has(envLevel)) {
return envLevel as LogLevel;
}
}
return 'warn';
}

const defaultHandler: LogHandler = (level, message) => {
switch (level) {
case 'debug':
case 'info':
console.log(message);
break;
case 'warn':
console.warn(message);
break;
case 'error':
console.error(message);
break;
}
};

let currentLevel: LogLevel | undefined;
let currentHandler: LogHandler | undefined;

function getLevel(): LogLevel {
return currentLevel ?? getDefaultLevel();
}

function getHandler(): LogHandler {
return currentHandler ?? defaultHandler;
}

function shouldLog(level: Exclude<LogLevel, 'silent'>): boolean {
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[getLevel()];
}

export function configureLogger(config: LoggerConfig): void {
if (config.level !== undefined) {
currentLevel = config.level;
}
if (config.handler !== undefined) {
currentHandler = config.handler;
}
}

export function resetLogger(): void {
currentLevel = undefined;
currentHandler = undefined;
}

export const logger = {
debug(message: string): void {
if (shouldLog('debug')) getHandler()('debug', message);
},
info(message: string): void {
if (shouldLog('info')) getHandler()('info', message);
},
warn(message: string): void {
if (shouldLog('warn')) getHandler()('warn', message);
},
error(message: string): void {
if (shouldLog('error')) getHandler()('error', message);
},
};
13 changes: 7 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '@/logger.js';

import {
stixBundleSchema,
Expand Down Expand Up @@ -103,14 +104,14 @@ export async function registerDataSource(registration: DataSourceRegistration):
throw new Error(`Unsupported source type: ${source}`);
}

console.log('Retrieved data');
logger.info('Retrieved data');

const parsedAttackObjects = parseStixBundle(rawData, parsingMode);
console.log('Parsed data.');
console.log(parsedAttackObjects.length);
logger.info('Parsed data.');
logger.info(`${parsedAttackObjects.length}`);

const model = new AttackDataModel(uniqueId, parsedAttackObjects);
console.log('Initialized data model.');
logger.info('Initialized data model.');

// Store the model and its unique ID in the dataSources map
dataSources[uniqueId] = { id: uniqueId, model };
Expand Down Expand Up @@ -217,7 +218,7 @@ function parseStixBundle(rawData: StixBundle, parsingMode: ParsingMode): AttackO
if (parsingMode === 'strict') {
throw new Error(`Bundle validation failed:\n${errors.join('\n')}`);
} else {
console.warn(`Bundle validation errors:\n${errors.join('\n')}`);
logger.warn(`Bundle validation errors:\n${errors.join('\n')}`);
}
return []; // Return empty array if bundle itself is invalid
}
Expand Down Expand Up @@ -310,7 +311,7 @@ function parseStixBundle(rawData: StixBundle, parsingMode: ParsingMode): AttackO
if (parsingMode === 'strict') {
throw new Error(`Validation errors:\n${errors.join('\n')}`);
} else {
console.warn(`Validation errors:\n${errors.join('\n')}`);
logger.warn(`Validation errors:\n${errors.join('\n')}`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/schemas/sro/relationship.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod/v4';
import { logger } from '@/logger.js';
import { attackBaseRelationshipObjectSchema } from '../common/index.js';
import {
createStixIdValidator,
Expand Down Expand Up @@ -324,7 +325,7 @@ export const relationshipChecks = (ctx: z.core.ParsePayload<RelationshipPartial>
ctx.value.relationship_type === 'detects' &&
ctx.value.target_ref.startsWith('attack-pattern--')
) {
console.warn(
logger.warn(
'DEPRECATION WARNING: x-mitre-data-component -> detects -> attack-pattern relationships are deprecated',
);
}
Expand Down
Loading