diff --git a/USAGE.md b/USAGE.md
index 8d5dadba..e87819cf 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -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
diff --git a/docusaurus/docs/how-to-guides/configure-logging.mdx b/docusaurus/docs/how-to-guides/configure-logging.mdx
new file mode 100644
index 00000000..a5a44293
--- /dev/null
+++ b/docusaurus/docs/how-to-guides/configure-logging.mdx
@@ -0,0 +1,121 @@
+import WorkInProgressNotice from '@site/src/components/WorkInProgressNotice';
+
+# How to Configure Logging
+
+
+
+**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.
+
+---
diff --git a/docusaurus/sidebars.ts b/docusaurus/sidebars.ts
index 61d357db..2348b099 100644
--- a/docusaurus/sidebars.ts
+++ b/docusaurus/sidebars.ts
@@ -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',
],
diff --git a/src/index.ts b/src/index.ts
index 3cba0298..e73a5ed4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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';
diff --git a/src/logger.ts b/src/logger.ts
new file mode 100644
index 00000000..730468b2
--- /dev/null
+++ b/src/logger.ts
@@ -0,0 +1,86 @@
+export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
+export type LogHandler = (level: Exclude, message: string) => void;
+
+export interface LoggerConfig {
+ level?: LogLevel;
+ handler?: LogHandler;
+}
+
+const LOG_LEVEL_PRIORITY: Record = {
+ debug: 0,
+ info: 1,
+ warn: 2,
+ error: 3,
+ silent: 4,
+};
+
+const VALID_LOG_LEVELS = new Set(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): 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);
+ },
+};
diff --git a/src/main.ts b/src/main.ts
index 0b63856c..df502868 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,6 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
+import { logger } from '@/logger.js';
import {
stixBundleSchema,
@@ -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 };
@@ -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
}
@@ -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')}`);
}
}
diff --git a/src/schemas/sro/relationship.schema.ts b/src/schemas/sro/relationship.schema.ts
index 2668c660..3a88ccfe 100644
--- a/src/schemas/sro/relationship.schema.ts
+++ b/src/schemas/sro/relationship.schema.ts
@@ -1,4 +1,5 @@
import { z } from 'zod/v4';
+import { logger } from '@/logger.js';
import { attackBaseRelationshipObjectSchema } from '../common/index.js';
import {
createStixIdValidator,
@@ -324,7 +325,7 @@ export const relationshipChecks = (ctx: z.core.ParsePayload
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',
);
}