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.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

Support를 위해 공통 Core JSON 클라이언트는 계정별 요청 ID, 시간, 간략한 작업명과 결과를 메모리에 최대 24시간, 2,000건 보관합니다. 클라이언트 로그를 선택하면 최대 20건의 요청 요약이 자동 첨부되며 DevTools나 추가 입력 항목은 필요하지 않습니다. URL, 본문, 인증 정보는 제외합니다. 이 기록은 Agent 재시작 후 복원되지 않습니다. 나중에 제출한 피드백에 요청 ID가 없어도 Core는 계정과 시간으로 서비스 및 게이트웨이 로그를 검색하며, 후보 일치만으로 문제의 원인을 확정하지 않습니다.

두 가지 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 필드 하나. 문제가 생기면 행을 읽고, 문제가 없으면 무시하면 됩니다. 이것이 설계의 전부입니다.

PostHog 핵심 제품 이벤트

PostHog를 활성화하고 관측 데이터 전송에 동의하면 Agent 게이트웨이는 최상위 실행마다 agent.run.started, agent.run.succeeded, agent.run.failed, agent.run.cancelled를 기록합니다. 개별 모델 요청이나 도구 호출을 별도 실행으로 세지 않습니다. 도우미 답변 전체를 성공적으로 복사하면 agent.result.adopted(action=copy)를 기록합니다.

거래 이벤트는 의도, 현물 미리보기 결과, 사용자 확인, 차단, 제출 접수, 제출 결과 불명, 실제 체결을 구분합니다. trade.submitted는 접수를 의미합니다. trade.execution.confirmed에는 명시적인 온체인 확인 상태 또는 양수인 선물 체결 수량이 필요합니다. 해시, HTTP 200, success: true만으로 체결을 판단하지 않습니다. 접수된 선물 주문은 제한된 백그라운드 조회로 확인하며 조회 실패 때문에 주문을 다시 제출하지 않습니다.

기존 VITE_POSTHOG_ENABLED, VITE_POSTHOG_PROJECT_TOKEN, VITE_POSTHOG_HOST와 동의 설정을 사용하며 데스크톱 설치 패키지에도 공개 설정을 전달합니다. 핵심 이벤트는 페이지 조회와 별도의 제한된 큐에서 중복을 제거합니다. 허용된 분류, 소요 시간, 금액 구간만 전송하며 프롬프트, 답변, 지갑 주소, 정확한 금액, 오류 원문은 보내지 않습니다. 계정 가명은 같은 설치 내에서 유지되지만 다른 기기나 재설치와 자동으로 통합되지 않습니다.

전송은 최선형 방식입니다. 오프라인이나 불명확한 응답은 거래 실패를 뜻하지 않습니다. 일부 크로스체인 응답은 완료로 계산하지 않으며 로컬 프로세스 종료 후 체결은 서버 거래 기록으로 확인해야 합니다.

목차