Files
trex-core/tests/runtime/test_logging_setup.py
genichinandClaude Opus 4.8 d8d9021260 feat(execution,obs): 호가단위 스냅(dcy-030/#28) + 운영 관측성(dcy-029/#29) + REL-007 환류
dcy-030 (issue #28, P0 체결 복구):
- REQ-083 KRX 호가단위(tick) 순수 유틸 (domain/pricing.py)
- REQ-070 CHG: 슬리피지 실효가 호가단위 스냅 — 상한 준수(매수 floor/매도 ceil)
  + band<1tick 하한/상한 보장 + 부동소수 epsilon 가드. 147700→148400/54500→54700.
- BAT-044 release-candidate

dcy-029 (issue #29, 운영 관측성):
- REQ-084 구조화 로깅 기반 (runtime/logging_setup.py): ISO8601 KST·레벨·Loki JSON
  ·correlation id(slot_id)
- REQ-085 주문 결과 로깅 (event=order_submit|fill|reject)
- REQ-087 거부 ERROR 가시성 (조용한 롤백 제거)
- REQ-086 청산 reason+actor 동반 노출 (안전망/전략 구분, 신규 필드 없음)
- REQ-088 운영 조회 CLI report (application/operator_report.py, 보유·오늘 체결
  ·실현 PnL net·대기, direct-DB)
- REQ-089 운영자 런북 docs/operations.md
- REQ-090 baseline 정합 (runtime-flows·project-structure·interface-contract)
- BAT-045(P0)·BAT-046(P1) release-candidate

REL-007: confirmed → feedback-captured (운영 인스턴스 라이브 검증 + #28/#29 환류
정규화). BAT-037~041 released.

검증: 537 passed, 47 skipped, 회귀 0, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:19:31 +09:00

102 lines
3.5 KiB
Python

"""구조화 로깅 기반 단위 테스트 (REQ-084, dcy-029 FR-1·FR-7)."""
import json
import logging
import pytest
from trex_core.runtime import logging_setup as ls
@pytest.fixture(autouse=True)
def _reset_corr():
ls.set_correlation_id("")
yield
ls.set_correlation_id("")
def _record(msg="hello", level=logging.INFO, **extra):
rec = logging.LogRecord("trex.test", level, __file__, 1, msg, None, None)
for k, v in extra.items():
setattr(rec, k, v)
return rec
class TestTextFormatter:
def test_has_iso8601_kst_timestamp_level_and_msg(self):
out = ls.TextFormatter().format(_record("boot ok"))
# ISO8601 with +09:00 KST offset, level, message
assert "+09:00" in out
assert "INFO" in out
assert "boot ok" in out
def test_includes_correlation_id_when_set(self):
ls.set_correlation_id("slot-123")
rec = _record("x")
ls._CorrelationFilter().filter(rec)
out = ls.TextFormatter().format(rec)
assert "cid=slot-123" in out
def test_omits_cid_when_unset(self):
rec = _record("x")
ls._CorrelationFilter().filter(rec)
out = ls.TextFormatter().format(rec)
assert "cid=" not in out
class TestJsonFormatter:
def test_is_valid_json_with_required_keys(self):
rec = _record("submitted")
ls._CorrelationFilter().filter(rec)
obj = json.loads(ls.JsonFormatter().format(rec))
assert obj["msg"] == "submitted"
assert obj["level"] == "INFO"
assert obj["logger"] == "trex.test"
assert "+09:00" in obj["ts"]
assert obj["correlation_id"] == ""
def test_correlation_id_propagates(self):
ls.set_correlation_id("slot-9")
rec = _record("y")
ls._CorrelationFilter().filter(rec)
obj = json.loads(ls.JsonFormatter().format(rec))
assert obj["correlation_id"] == "slot-9"
def test_extra_fields_included(self):
rec = _record("z", symbol="005930")
ls._CorrelationFilter().filter(rec)
obj = json.loads(ls.JsonFormatter().format(rec))
assert obj["symbol"] == "005930"
class TestConfigureAndContext:
def test_configure_text_default(self, monkeypatch, capsys):
monkeypatch.delenv("TREX_LOG_FORMAT", raising=False)
ls.configure_logging(level=logging.INFO)
logging.getLogger("trex.x").info("via root")
err = capsys.readouterr().err
assert "via root" in err and "INFO" in err and "+09:00" in err
def test_configure_json_format(self, monkeypatch, capsys):
monkeypatch.setenv("TREX_LOG_FORMAT", "json")
ls.configure_logging(level=logging.INFO)
with ls.correlation_id("slot-77"):
logging.getLogger("trex.x").info("jsonline")
line = [ln for ln in capsys.readouterr().err.splitlines() if "jsonline" in ln][-1]
obj = json.loads(line)
assert obj["msg"] == "jsonline"
assert obj["correlation_id"] == "slot-77"
# cleanup: restore text formatter for other tests
monkeypatch.delenv("TREX_LOG_FORMAT", raising=False)
ls.configure_logging(level=logging.INFO)
def test_correlation_id_context_manager_restores(self):
ls.set_correlation_id("outer")
with ls.correlation_id("inner"):
assert ls.get_correlation_id() == "inner"
assert ls.get_correlation_id() == "outer"
def test_configure_is_idempotent_single_handler(self):
ls.configure_logging()
ls.configure_logging()
assert len(logging.getLogger().handlers) == 1