관찰 가능성
실행 중인 Agent의 로깅, 감사, 디버깅
Agent가 잘못된 동작을 했을 때는 그 원인을 파악해야 합니다. Minara Agent는 구조화 로그, 감사 로그, 티어 이벤트, 이렇게 세 가지 정보 스트림을 제공합니다. 각 스트림은 고유한 목적을 가집니다. 이 페이지에서는 각 스트림에 어떤 정보가 담기는지, 그리고 흔한 장애 상황을 어떻게 조사하는지 설명합니다.
왜 하나의 큰 로그가 아닌 세 개의 스트림인가요? Agent 의 오동작은 세 가지 형태로 나타납니다. 인프라 장애(LLM 호출 실패, DB 잠김), Agent의 잘못된 동작(잘못된 인수로 도구 호출), 또는 권한 로직이 예상치 못하게 무언가를 차단한 경우입니다. 이 세 가지를 모두 하나의 스트림에 담으면 가독성이 떨어집니다. 스트림을 분리하면 즉시 적합한 테이블로 접근할 수 있습니다. 동작 문제는
audit, 권한 문제는tier_events, 인프라 문제는 구조화 로그를 확인하면 됩니다. 세 스트림 모두trace_id를 공유하므로, 전체 흐름을 파악해야 할 때 하나의 스토리로 연결할 수 있습니다.
실제 사례 확인: 6단계 안전 스택은
자금 이동 관련 호출마다 tier_events에 행을 기록합니다. 이를 통해 거래가 실행되기 전 모든 게이트를 통과했음을 사후에 증명할 수 있습니다.
세 가지 스트림
| 스트림 | 저장 위치 | 목적 |
|---|---|---|
| 구조화 로그 | $dataDir/logs/*.ndjson 및 stdout | 운영 가시성, 시작 상태, LLM 호출 |
| 감사 로그 | SQLite audit 테이블 | "Agent가 어떤 도구를 호출했고 결과는 무엇이었는가" |
| 티어 이벤트 | SQLite tier_events 테이블 | "이 호출이 허용되거나 차단된 이유는 무엇인가" |
일반적인 기준은 다음과 같습니다. 동작을 디버깅할 때는 감사 로그부터, 인프라를 디버깅할 때는 구조화 로그부터, 권한을 디버깅할 때는 티어 이벤트부터 시작하십시오.
구조화 로그
apps/agent/src/core/logger.ts는
최소화된 JSON 로거를 제공합니다.
logger.info("skills/registry", "skill_registered", { id });
logger.warn("agent/loop", "max_iterations_reached", { session_id });
logger.error("llm/anthropic-wire", "cache_miss", { reason });각 줄은 다음 구조의 JSON 객체입니다.
{
"ts": "2026-04-14T12:34:56.789Z",
"level": "info",
"category": "skills/registry",
"event": "skill_registered",
"correlation_id": "turn_abc123",
"trace_id": "t_xyz",
"data": { "id": "minara.core" }
}correlation ID는 턴 실행을 감싸는 withCorrelation(id, fn) 래퍼에서 생성됩니다. 턴 내부에서 생성되는 모든 로그 줄은 AsyncLocalStorage를 통해 동일한 ID를 상속받습니다. 따라서 grep correlation_id=turn_abc123 $dataDir/logs/*.ndjson 명령으로 특정 턴의 전체 내역을 조회할 수 있습니다.
로그 로테이션
로그는 $dataDir/logs/ 하위에 날짜 기반 일별 로테이션으로 기록됩니다. 다음 환경 변수로 임계값을 설정할 수 있습니다.
LOG_LEVEL(debug/info/warn/error, 기본값warn)
LOG_LEVEL=debug로 설정해도 안전합니다. 구조화 형식이므로 jq로 불필요한 내용을 필터링할 수 있습니다. 다만 로그 양이 약 4배 증가합니다.
감사 로그: 신뢰할 수 있는 단일 출처
모든 도구 호출은 apps/agent/src/core/audit-log-hook.ts의
auditLogHook을 통해 처리됩니다. 테이블 스키마는 다음과 같습니다.
CREATE TABLE audit (
id TEXT PRIMARY KEY,
session_id TEXT,
trace_id TEXT,
tool_name TEXT,
tool_set TEXT,
args_json TEXT, -- redacted
result_json TEXT,
blocked INTEGER,
block_reason TEXT,
permission_tier INTEGER,
source TEXT, -- user | cron | autopilot | delegation
duration_ms INTEGER,
created_at INTEGER
);
CREATE VIRTUAL TABLE audit_fts USING fts5(
tool_name, block_reason, args_json, content='audit'
);FTS5 덕분에 전체 기록을 다음과 같이 검색할 수 있습니다.
SELECT created_at, tool_name, blocked, block_reason
FROM audit
WHERE audit MATCH 'withdraw'
ORDER BY created_at DESC
LIMIT 50;모든 조사는 여기서 시작합니다. 사용자가 "Agent가 이상한 동작을 했다"고 보고하면, 첫 번째 조치는 해당 세션의 감사 행을 순서대로 조회하는 것입니다. LLM의 추론 텍스트, 도구 인수, 원시 도구 출력, 타임스탬프가 모두 기록되어 있습니다.
리댁션 대상
tools/_shared/result.ts의
리댁터는 감사 로그에 기록되기 전에 알려진 민감 키(api_key, secret, password,
token, private_key, mnemonic, seed)를 ***로 마스킹합니다. 비밀 정보는 도구 인수가 아닌 팩토리 시점의 process.env에서 가져온다는 규칙과 결합하면, 감사 로그는 지원팀과 공유하거나 버그 리포트에 첨부해도 최소한의 검토만으로 안전합니다.
티어 이벤트: 차단된 이유
core/permission-tier-hook.ts는
허용 여부와 관계없이 모든 결정에 대해 tier_events에 행을 기록합니다.
CREATE TABLE tier_events (
id TEXT PRIMARY KEY,
tool_name TEXT,
source TEXT,
tier INTEGER,
allow_ceiling INTEGER,
decision TEXT, -- allow | block | pending_confirmation
reason TEXT,
trace_id TEXT,
created_at INTEGER
);감사 로그는 무슨 일이 일어났는지를 알려줍니다. 티어 이벤트는 권한 시스템이 그 결정을 내린 이유를 알려줍니다. 두 테이블은 trace_id로 조인하며, 종종 함께 조회됩니다.
SELECT a.tool_name, a.blocked, te.decision, te.reason
FROM audit a
LEFT JOIN tier_events te ON te.trace_id = a.trace_id
AND te.tool_name = a.tool_name
WHERE a.session_id = ?
ORDER BY a.created_at;Correlation ID와 Trace ID
두 가지 ID가 사용됩니다.
correlation_id는 턴 단위입니다. Agent 루프가 각 턴 시작 시 생성하며, 구조화 로그와 감사 행에 모두 표시됩니다.trace_id는 워크플로 실행 또는 시그널 단위입니다.SignalContext나WorkflowInstance에서 시작하여 서브 Agent 위임을 포함한 모든 하위 도구 호출로 전파됩니다.
일반적인 사용자 채팅 턴에는 새로운 correlation_id가 부여되며 trace_id는 없습니다. 크론 실행에는 두 ID 모두 있습니다. trace_id를 사용하면 "14:03에 발생한 BTC 알림이 수명 전체에 걸쳐, 그리고 생성한 서브 Agent를 포함하여 어떤 동작을 했는가"를 조회할 수 있습니다.
일반적인 조사 시나리오
"Agent가 X 호출을 거부한 이유는?"
SELECT tool_name, decision, reason, created_at
FROM tier_events
WHERE tool_name = 'swap'
AND decision = 'block'
ORDER BY created_at DESC
LIMIT 10;reason을 확인하십시오. 가장 흔한 값은 다음과 같습니다.
tier_exceeds_ceiling. 스킬이 활성화되지 않았거나, 해당 턴의allowRiskTier가 도구의 티어보다 낮습니다.analysis_to_trade_boundary. 해당 턴에서 이미 분석 호출이 이루어졌습니다. 거래 호출은 별도의 사용자 메시지로 전송해야 합니다.daily_cap_exceeded.daily_spend와 이번 호출의 명목 금액의 합이MINARA_DAILY_CAP_USD를 초과합니다.kill_switch_active. 사용자 또는 Agent가kill을 호출했습니다.tool_set_not_allowed. 해당 턴의allowedToolSets에서 이 도구가 제외되었습니다.
"이 턴이 느린 이유는?"
SELECT tool_name, AVG(duration_ms) AS avg_ms, COUNT(*) AS n
FROM audit
WHERE session_id = ?
GROUP BY tool_name
ORDER BY avg_ms DESC;correlation_id로 필터링한 구조화 로그와 함께 조회하면 LLM 호출 소요 시간과 캐시 적중률을 파악할 수 있습니다.
"프롬프트 캐시가 작동했는가?"
로그에서 llm.cache_read_input_tokens를 검색하십시오. 세션 전반에 걸쳐 이 값이 0이라면, 캐시 가능한 프롬프트 블록이 안정적이지 않은 것입니다. 즉, 턴마다 프롬프트를 다시 생성하고 있어 캐싱의 효과가 없는 상태입니다. LLM 통합 문서를 참고하십시오.
"Autopilot이 밤새 무엇을 했는가?"
SELECT created_at, tool_name, blocked, substr(result_json, 1, 200)
FROM audit
WHERE source = 'autopilot'
AND created_at > ?
ORDER BY created_at;헬스 엔드포인트
HTTP 게이트웨이는 다음 엔드포인트를 제공합니다.
GET /healthz는 프로세스가 실행 중이고 DB를 열 수 있으면200을 반환하는 라이브니스 프로브입니다.GET /status는 DB 통계, 스킬 수, 활성 트리거, 마지막 LLM 호출 성공 타임스탬프 등 레디니스 세부 정보를 반환합니다.
정확한 스키마는 API 참조 문서를 확인하십시오.
외부 도구로 내보내기
Loki, Datadog, Grafana Cloud 등 외부 도구에 로그를 전송하려면 stdout을 파이프하십시오.
docker run minara 2>&1 | vector --config vector.toml모든 stdout 줄은 유효한 NDJSON입니다. 별도의 "구조화 로그" 내보내기 경로는 없습니다. stdout이 정식 출력 채널입니다.
관찰 가능성은 의도적으로 단순하게 설계되었습니다. 테이블 세 개, 로그 형식 하나, correlation 필드 하나, trace 필드 하나. 문제가 생기면 행을 읽고, 문제가 없으면 무시하면 됩니다. 이것이 설계의 전부입니다.