MINARA

관찰 가능성

실행 중인 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.tsauditLogHook을 통해 처리됩니다. 테이블 스키마는 다음과 같습니다.

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 는 워크플로 실행 또는 시그널 단위입니다. SignalContextWorkflowInstance에서 시작하여 서브 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 필드 하나. 문제가 생기면 행을 읽고, 문제가 없으면 무시하면 됩니다. 이것이 설계의 전부입니다.

목차