봇 로그
애플리케이션의 봇 탭에서 실행 중인 봇에 최근 로그 줄을 요청할 수 있으므로, 봇이 돌아가는 머신에 로그인하지 않고도 무슨 일이 있었는지 볼 수 있습니다. 이 페이지는 봇이 이에 응답하기 위해 구현하는 프로토콜을 설명합니다.
동작 방식
- 개발자(소유자, 또는 팀의 개발자 이상)가 봇 탭에서 로그 요청을 누르고 기간과 줄 수를 고릅니다.
- GameVox가 봇에
GAMEVOX_LOG_REQUEST라는 게이트웨이 디스패치를 보냅니다. 같은 애플리케이션의 살아 있는 게이트웨이 세션에만 전달됩니다. - 봇은 그 기간의 로그 줄을 모아 REST 호출 한 번으로 업로드합니다. 포털은 도착하는 즉시 보여 줍니다.
- 2분 안에 아무것도 도착하지 않으면 포털은 봇이 응답하지 않았다고 표시합니다. 이 이벤트를 모르는 봇은 그냥 무시하므로, 이 이벤트를 전혀 모르는 라이브러리에서도 깨지는 것은 없습니다.
게이트웨이 이벤트
t가 GAMEVOX_LOG_REQUEST인 일반 디스패치(opcode 0)입니다. 다른 디스패치처럼 시퀀스 번호가 있으며, 재개(resume) 시 다시 전달됩니다.
{
"op": 0,
"s": 4821,
"t": "GAMEVOX_LOG_REQUEST",
"d": {
"request_id": "6f0c2d1e-8a54-4b7e-9c3d-2a1f5e7b9d40",
"since": "2026-10-30T11:45:00Z",
"max_lines": 2000,
"nonce": "3f9a0c...e1b7",
"expires_at": "2026-10-30T12:02:00Z"
}
} request_id: 요청을 식별하며, 업로드 URL의 일부입니다.since: RFC 3339 시각. 이 시각 이후의 줄을 보내세요.max_lines: 원하는 최대 줄 수(50~20,000). 더 많다면 최신 줄을 보내세요.nonce: 이 요청만을 위한 일회용 비밀 값. 업로드할 때 돌려보내세요. 요청을 받은 봇이 업로드했다는 증거가 됩니다.expires_at: 이 시각이 지나면 업로드가 거부되는 RFC 3339 시각(요청 2분 뒤).
로그 줄 업로드하기
다른 모든 봇 호출과 같은 인증(Authorization: Bot <token>)으로 요청 한 번에 응답합니다. 일반적인 형식은 JSON입니다:
POST /api/v10/applications/@me/log-requests/6f0c2d1e-8a54-4b7e-9c3d-2a1f5e7b9d40
Authorization: Bot <token>
Content-Type: application/json
{
"nonce": "3f9a0c...e1b7",
"lines": [
{ "ts": "2026-10-30T11:58:02.114Z", "level": "info", "msg": "synced 12 guilds" },
{ "ts": "2026-10-30T11:58:09.870Z", "level": "error", "msg": "poll failed: timeout" }
]
}
HTTP/1.1 204 No Content 각 줄은 {ts, level, msg}입니다. ts는 RFC 3339 시각, level은 debug, info, warn, error 중 하나, msg는 텍스트입니다. 객체 대신 그냥 문자열도 받습니다.
일반 텍스트도 됩니다. Content-Type: text/plain으로 한 줄에 하나씩 보내고, nonce는 X-GameVox-Log-Nonce 헤더에 담으세요. 이렇게 보낸 줄에는 타임스탬프와 레벨이 없습니다(GameVox가 줄 안의 단어로 레벨을 추측합니다). 이 헤더는 JSON에서도 쓸 수 있으며, 본문 필드는 편의를 위한 것일 뿐입니다.
제한과 규칙
- 본문은 최대 2 MiB이고, 요청 하나에 최대 20,000줄까지 보관됩니다. 줄 수 한도를 넘으면 최신 줄이 남고, 더 큰 본문은 413으로 거부됩니다.
- 요청에는 한 번만 응답할 수 있습니다. 두 번째 업로드는 409를 받습니다.
- 요청 대상인 애플리케이션만 응답할 수 있습니다. 다른 애플리케이션의 요청 id는 알 수 없는 id처럼 보입니다(404).
- 업로드는
expires_at전에 도착해야 합니다(그 뒤에는 410). - nonce가 일치해야 합니다(아니면 403).
- 요청은 애플리케이션당 30초에 한 번, 하루 30번으로 제한됩니다. 번들은 7일 동안 보관된 뒤 삭제됩니다.
- 로그 텍스트는 일반 텍스트로만 표시됩니다. 제어 문자는 제거되고 각 줄은 4,000자에서 잘립니다.
오류
- 400: 본문이 올바른 JSON이 아니거나 nonce가 없습니다.
- 403: nonce가 일치하지 않습니다.
- 404: 여러분의 애플리케이션에 해당하는 요청이 없습니다.
- 409: 이미 응답한 요청입니다.
- 410: 요청이 만료되었습니다.
- 413: 본문이 2 MiB보다 큽니다.
- 429: 다른 라우트와 마찬가지로 봇이 요청 한도에 걸렸습니다.
보내기 전에 비밀 값을 지우세요
로그는 여러분의 머신 밖으로 나가므로, 나가면 안 되는 것은 지우세요. 봇 토큰과 그 밖의 자격 정보, Authorization 헤더, URL 속 비밀번호, 이메일 주소, 사용자의 IP 주소 등입니다. GameVox 자체 봇은 업로드 전에 이 모두를 처리합니다.
GameVox도 도착 시점에 가리기를 하며(토큰, Authorization 값, 키 이름이 비밀 값을 뜻하는 key=value 쌍, URL 속 비밀번호, 긴 16진 문자열, 이메일 주소), 몇 개의 값을 숨겼는지 보는 사람에게 알려 줍니다. 이것은 안전망일 뿐, 직접 가리기를 건너뛸 이유는 아닙니다. 여러분의 메시지에서 무엇이 비공개인지 GameVox는 알 수 없기 때문입니다.
예제
대부분의 라이브러리는 모델링하지 않은 이벤트를 원시 훅으로 넘겨 줍니다. discord.py의 on_socket_raw_receive, discord.js의 raw 이벤트입니다. 최근 수천 줄을 링 버퍼에 보관해 두었다가, 이벤트가 오면 요청된 부분을 보내세요.
# discord.py: keep recent lines, answer GAMEVOX_LOG_REQUEST
import collections, json, logging
from datetime import datetime, timezone
RING = collections.deque(maxlen=5000)
class Ring(logging.Handler):
def emit(self, record):
RING.append({"ts": datetime.fromtimestamp(record.created, timezone.utc).isoformat(),
"level": record.levelname.lower(),
"msg": redact(self.format(record))})
logging.getLogger().addHandler(Ring())
@client.event
async def on_socket_raw_receive(msg):
packet = json.loads(msg)
if packet.get("t") != "GAMEVOX_LOG_REQUEST":
return
d = packet["d"]
lines = [l for l in RING if l["ts"] >= d["since"]][-d["max_lines"]:]
await session.post(
f"https://app-api.gamevox.com/api/v10/applications/@me/log-requests/{d['request_id']}",
headers={"Authorization": f"Bot {TOKEN}"},
json={"nonce": d["nonce"], "lines": lines}) 호환성
이 이벤트는 새로 추가되었으며 선택 사항입니다. 라이브러리는 모르는 디스패치를 무시하므로, Discord 라이브러리로 작성한 봇은 바꾸지 않아도 그대로 동작합니다. GameVox 자체 봇(League of Legends, VALORANT, Deadlock, Valheim, WARDOGS, WoW 길드, Tickets, Tibia)은 공용 봇 라이브러리를 통해 이를 지원합니다.