Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

```bash
# 빌드 (CGO 필수 — go-sqlite3)
CGO_ENABLED=1 go build -o webhook-relay ./cmd/server/
CGO_ENABLED=1 go build -o relaybox ./cmd/server/

# 전체 테스트 (race detector 포함)
go test -race ./... -timeout 60s
Expand All @@ -16,7 +16,7 @@ go test -race ./internal/adapter/output/sqlite/...
go test -race ./test/e2e/...

# 특정 테스트 함수 하나만 실행
go test -race -run TestDeliveryWorker_DeliverSuccess ./internal/application/service/
go test -race -run TestRelayWorker_DeliverSuccess ./internal/application/service/

# 정적 분석
go vet ./...
Expand Down Expand Up @@ -47,11 +47,11 @@ cmd/server/main.go ← DI 조립, cobra CLI

| 경로 | 역할 |
|------|------|
| `internal/domain/` | 엔티티(`Alert`, `Channel`), 열거형(`SourceType`, `AlertStatus`, `ChannelType`), 센티넬 에러, 템플릿 렌더링 |
| `internal/application/port/input/` | `ReceiveAlertUseCase` 인터페이스 |
| `internal/application/port/output/` | `AlertRepository`, `AlertQueue`, `AlertSender`, `SenderRegistry`, `RouteConfigReader` 인터페이스 |
| `internal/application/service/` | `AlertService`(Receive), `DeliveryWorker`(Start) |
| `internal/config/` | Viper 기반 YAML 로더, `InMemoryRouteConfigReader`, hot-reload(`Watch`) |
| `internal/domain/` | 엔티티(`Message`, `Output`), 열거형(`InputType`, `MessageStatus`, `OutputType`), 센티넬 에러, 템플릿 렌더링 |
| `internal/application/port/input/` | `ReceiveMessageUseCase` 인터페이스 |
| `internal/application/port/output/` | `MessageRepository`, `MessageQueue`, `OutputSender`, `OutputRegistry`, `RuleConfigReader` 인터페이스 |
| `internal/application/service/` | `MessageService`(Receive), `RelayWorker`(Start) |
| `internal/config/` | Viper 기반 YAML 로더, `InMemoryRuleConfigReader`, hot-reload(`Watch`) |
| `internal/adapter/input/http/` | chi 라우터, RFC 7807 에러, `X-API-Version` 헤더 미들웨어 |
| `internal/adapter/input/websocket/` | gorilla/websocket 인바운드 핸들러 |
| `internal/adapter/output/sqlite/` | sqlc 기반 SQLite 저장소 |
Expand All @@ -66,23 +66,23 @@ cmd/server/main.go ← DI 조립, cobra CLI
모든 열거형은 `type X string` + 대문자 상수(`"BESZEL"`, `"PENDING"` 등). 별도 `MarshalJSON` 불필요.

### 라우팅 키
`InMemoryRouteConfigReader`는 routes를 **source type**(e.g. `"BESZEL"`)으로 인덱싱한다. `Update()`에서 `sourceIDsourceType` 변환을 수행하므로, `GetChannels`는 반드시 `string(alert.Source)`(타입 값)를 넘겨야 한다.
`InMemoryRuleConfigReader`는 rules를 **input type**(e.g. `"BESZEL"`)으로 인덱싱한다. `Update()`에서 `inputIDinputType` 변환을 수행하므로, `GetOutputs`는 반드시 `string(msg.Input)`(타입 값)를 넘겨야 한다.

### 큐 at-least-once 보장
파일 큐는 `Dequeue` 시 `.json` → `.json.processing` 으로 rename. `Ack`는 `.processing` 삭제, `Nack`는 원래 이름으로 rename 복구.

### AckFunc / NackFunc
`AlertQueue.Dequeue`는 `(domain.Alert, AckFunc, NackFunc, error)`를 반환한다. `AckFunc`와 `NackFunc`는 `output` 패키지에 정의된 함수 타입.
`MessageQueue.Dequeue`는 `(domain.Message, AckFunc, NackFunc, error)`를 반환한다. `AckFunc`와 `NackFunc`는 `output` 패키지에 정의된 함수 타입.

### HTTP API
- URL versioning 없이 `X-API-Version` 응답 헤더 사용
- 에러 포맷: RFC 7807 (`application/problem+json`)
- `POST /sources/{sourceId}/alerts` → 201 + `Location: /sources/{sourceId}/alerts/{alertId}`
- `POST /inputs/{inputId}/messages` → 201 + `Location: /inputs/{inputId}/messages/{messageId}`
- Bearer token 인증 (`Authorization: Bearer <secret>`)
- WebSocket: `GET /sources/{sourceId}/alerts/ws`
- WebSocket: `GET /inputs/{inputId}/messages/ws`

### sqlc
`internal/adapter/output/sqlite/db/` 는 자동 생성 코드. `query.sql` / `schema.sql` 수정 후 `sqlc generate` 재실행. 직접 편집 금지.

### DI 조립
`cmd/server/main.go`의 `runServer()` 함수가 전체 어댑터를 조립한다. `configSourceResolver`는 config 파일 기반으로 `SourceResolver` 인터페이스를 구현한다.
`cmd/server/main.go`의 `runServer()` 함수가 전체 어댑터를 조립한다. `configInputResolver`는 config 파일 기반으로 `InputResolver` 인터페이스를 구현한다.
115 changes: 62 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,47 @@
# webhook-relay
# relaybox

모니터링 알람을 수신하여 외부 채널로 전달하는 경량 웹훅 릴레이 허브.
Generic relay hub: receives any inbound protocol/format and delivers to outbound channels (CEL/Expr expression filter/transform/route rules planned).

```
beszel / dozzle → webhook-relay → Slack / Discord / 커스텀 Webhook
any inbound (HTTP REST / WebSocket / TCP / ...)
CEL / Expr expression filter + transform + route
any outbound (Webhook / Slack / Discord / ...)
```

## 주요 기능
## Features

- **다중 소스 수신** — HTTP REST + WebSocket 인바운드
- **YAML 템플릿 변환** — Go `text/template` 기반 페이로드 변환
- **at-least-once 전달** — 파일 큐 기반, 재시작 후에도 메시지 유실 없음
- **지수 백오프 재시도** — 채널별 `retryCount` / `retryDelayMs` 설정
- **설정 핫리로드** — 재시작 없이 channels / routes 변경 반영
- **Bearer token 인증** — 소스별 독립 시크릿
- **Multi-protocol inbound** — HTTP REST + WebSocket (TCP planned)
- **Expression-based routing** — CEL/Expr filter and transform rules per route (planned)
- **Template transformation** — Go `text/template` payload rendering
- **at-least-once delivery** — file-queue backed, survives restarts
- **Exponential backoff retry** — per-channel `retryCount` / `retryDelayMs`
- **Hot config reload** — change outputs / rules without restart
- **Bearer token auth** — per-input independent secrets

## 빠른 시작
## Quick Start

### 사전 요구사항
### Prerequisites

- Go 1.25+
- GCC (go-sqlite3 CGO 빌드 필요)
- GCC (required for go-sqlite3 CGO build)

```bash
# 빌드
CGO_ENABLED=1 go build -o webhook-relay ./cmd/server/
# Build
CGO_ENABLED=1 go build -o relaybox ./cmd/server/

# 설정 파일 준비
# Prepare config
cp internal/config/config.example.yaml config.yaml
# config.yaml 편집 후
# Edit config.yaml, then:

# 서버 시작
./webhook-relay start --config config.yaml
# Start server
./relaybox start --config config.yaml
```

## 설정
## Configuration

`config.yaml` 예시:
`config.yaml` example:

```yaml
server:
Expand All @@ -46,98 +51,102 @@ log:
level: info # debug | info | warn | error
format: json # json | text

sources:
inputs:
- id: beszel
type: BESZEL
secret: "your-secret"

channels:
outputs:
- id: ops-webhook
type: WEBHOOK
url: "https://hooks.example.com/xyz"
template: '{"text": "{{ .Source }}: {{ .Payload }}"}'
retryCount: 3
retryDelayMs: 1000

routes:
- sourceId: beszel
channelIds: [ops-webhook]
rules:
- inputId: beszel
outputIds: [ops-webhook]

storage:
type: SQLITE
path: "./data/webhook-relay.db"
path: "./data/relaybox.db"

queue:
type: FILE
path: "./data/queue"
workerCount: 2
```

### 템플릿 변수
### Template Variables

| 변수 | 설명 |
|------|------|
| `{{ .ID }}` | 알람 ULID |
| `{{ .Source }}` | 소스 타입 (`BESZEL`, `DOZZLE`) |
| `{{ .Payload }}` | 원본 JSON 페이로드 (문자열) |
| `{{ .CreatedAt }}` | 수신 시각 (`time.Time`) |
| Variable | Description |
|----------|-------------|
| `{{ .ID }}` | Alert ULID |
| `{{ .Source }}` | Source type (`BESZEL`, `DOZZLE`, etc.) |
| `{{ .Payload }}` | Raw JSON payload (string) |
| `{{ .CreatedAt }}` | Receive time (`time.Time`) |

## API

### 알람 수신
### Receive Message

```
POST /sources/{sourceId}/alerts
POST /inputs/{inputId}/messages
Authorization: Bearer <secret>
Content-Type: application/json

{"host": "server1", "status": "down"}
```

응답 `201 Created`:
Response `201 Created`:
```json
{"id": "01J...", "status": "PENDING"}
```

### WebSocket 수신
### WebSocket Inbound

```
GET /sources/{sourceId}/alerts/ws
GET /inputs/{inputId}/messages/ws
Authorization: Bearer <secret>
```

연결 후 JSON 메시지 전송 시 HTTP POST와 동일하게 처리.
JSON messages sent after connect are processed identically to HTTP POST.

### 헬스체크
### Health Check

```
GET /healthz
→ 200 OK
```

모든 응답에 `X-API-Version` 헤더가 포함된다.
All responses include an `X-API-Version` header.

## 아키텍처
## Architecture

헥사고날 아키텍처(Ports & Adapters). 의존성은 항상 안쪽(domain)으로만 흐른다.
Hexagonal architecture (Ports & Adapters). Dependencies always flow inward toward domain.

```
[ HTTP / WebSocket ]
application/service
[ SQLite repo ] [ File queue ] [ Webhook sender ]
domain (0 deps)
application/port/{input,output} ← interface definitions
application/service ← business logic
adapter/{input,output} ← external world connections
cmd/server/main.go ← DI wiring, cobra CLI
```

## 개발
## Development

```bash
# 전체 테스트 (race detector)
# Full test suite (race detector)
go test -race ./... -timeout 60s

# 정적 분석
# Static analysis
go vet ./...

# sqlc 재생성 (SQL 변경 시)
# Regenerate sqlc (after SQL changes)
cd internal/adapter/output/sqlite && sqlc generate
```
Loading
Loading