Ticker 문서
v0.2.1 기준컬렉터를 띄우고, 서비스들이 스스로 등록하게 하고, Slack 알림까지 붙이는 데 필요한 전부를 담았습니다. 원칙은 하나예요 — 기본만으로 바로 뜨고, 기본을 넘어서는 건 전부 꺼져 있습니다.
퀵스타트 — 5분
Ticker는 Maven Central에 올라간 두 개의 Spring Boot 스타터입니다 (io.stevelabs, Apache-2.0). 컬렉터 하나 + 감시할 앱마다 클라이언트 하나.
1. 컬렉터 띄우기
빈 Spring Boot 앱(또는 기존 운영용 앱)에 서버 스타터를 추가하고 켜면 끝. 시작한 뒤 http://localhost:8080 을 열면 상태 벽과 내장 UI가 그 앱에서 바로 서빙됩니다.
// build.gradle.kts
dependencies {
implementation("io.stevelabs:ticker-server-spring-boot-starter:0.2.1")
}<!-- pom.xml -->
<dependency>
<groupId>io.stevelabs</groupId>
<artifactId>ticker-server-spring-boot-starter</artifactId>
<version>0.2.1</version>
</dependency># application.yml
ticker:
server:
enabled: true
targets: # optional: things that can't self-register
- { name: edge-nginx, type: HTTP, url: http://edge-nginx/healthz }2. 서비스가 스스로 등록하게 하기
감시할 각 Spring Boot 앱에 클라이언트 스타터를 추가하세요 — 필요한 건 컬렉터 URL 하나뿐입니다. 감시 대상 앱의 Boot 버전에 맞는 스타터를 고르세요 (Boot 4.x → ticker-client-spring-boot-starter, Boot 3.2+ → ticker-client-spring-boot3-starter). 컬렉터 자체는 항상 Boot 4 / Java 21로 돌지만, Boot 3 앱도 HTTP로 문제없이 등록합니다.
dependencies {
// on a Spring Boot 4.x app:
implementation("io.stevelabs:ticker-client-spring-boot-starter:0.2.1")
// …or on a Spring Boot 3.2+ app instead (same config, same behaviour):
// implementation("io.stevelabs:ticker-client-spring-boot3-starter:0.2.1")
}# application.yml — that's the whole client config
spring.application.name: orders-api
ticker.client.collector-url: http://ticker-collector:8080클라이언트를 못 넣는 대상(nginx, 외부 API)은? 벽에서 클릭으로 HTTP 모니터를 추가하거나, targets 설정에 적으면 됩니다.
3. (선택) 코드로 설정
타깃과 알림 규칙은 TickerConfigurer 빈으로도 정의할 수 있습니다 — 설정을 코드리뷰에 태우고 싶을 때.
@Bean
fun tickerConfig() = TickerConfigurer { t ->
t.addTarget("payments-api", ServiceType.HTTP, "https://payments/health", tags = listOf("prod"))
t.configureAlert("cpu-process", threshold = 0.70, forSeconds = 15)
}동작 방식
- 01클라이언트 스타터가 앱 시작 시 컬렉터에 POST로 자기를 등록하고, 30초마다 하트비트로 재등록합니다 — 그래서 컬렉터를 재배포해도 벽이 저절로 복원돼요. DB가 필요 없는 이유입니다.
- 02컬렉터는 가상 스레드로 모든 타깃을 병렬 폴링합니다. SPRING 타깃은 /actuator/health + 화이트리스트에 있는 지표만 읽고(env, configprops, heapdump는 절대 안 읽음), HTTP 타깃은 GET 한 번에 2xx면 정상.
- 03한 번 실패했다고 DOWN이 되지 않습니다 — N회 연속 실패(기본 3)여야 하고, 알림엔 쿨다운이 있어요. 순단 한 번으로 새벽에 페이지되는 일은 없습니다.
- 04현재 상태와 최근 히스토리는 전부 메모리에 있습니다. 그래프의 5분~7일 범위가 필요할 때만 히스토리를 옵트인으로 켜세요 (H2 파일 기본, MySQL/PostgreSQL 선택).
설정 레퍼런스
모든 프로퍼티는 표준 Spring @ConfigurationProperties입니다 — application.yml, 환경변수(relaxed binding: ticker.server.public-url ↔ TICKER_SERVER_PUBLIC_URL), 또는 TickerConfigurer 빈으로 설정하세요. IDE 자동완성과 인라인 문서가 전부 번들되어 있습니다.
설정 없이 띄우면: 벽 + 드릴다운, 인메모리, 알림 없음, DB 없음. 아래 표의 기본값이 곧 그 상태입니다.
ticker.server.* — 컬렉터 자신
| 프로퍼티 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| ticker.server.enabled | boolean | true | 컬렉터 활성화 — REST API, 내장 UI, 폴러가 함께 켜집니다. |
| ticker.server.base-path | String | — | UI + API 전체를 /ticker 같은 접두사 아래로 이동 (게이트웨이 뒤에서 /api가 겹칠 때). /actuator는 외부 프로브를 위해 제자리에 남습니다. |
| ticker.server.public-url | String | — | 사람들이 이 Ticker를 여는 외부 URL (예: https://ops.acme.com/ticker). Slack 알림의 '보드 열기' 링크에 사용 — Grafana의 root_url과 같은 개념. 미설정이면 링크만 생략됩니다. |
| ticker.server.registration-expiry | Duration | 0 (off) | 옵트인: 하트비트가 이 시간 동안 끊긴 자가등록 인스턴스를 퇴출 (예: 10m). 기본은 꺼짐 — 크래시한 인스턴스는 벽에 빨갛게 남아야 하니까요. 오토스케일링 churn에만 켜세요. |
| ticker.server.exclude-self-requests | boolean | true | 컬렉터 자신의 모니터링 트래픽(/actuator 셀프 폴링 + /api UI 폴링)을 자기 http.server.requests에서 제외 — 'self' 타일이 진짜 트래픽을 보여줍니다. |
ticker.poll.* — 헬스 폴링
| 프로퍼티 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| ticker.poll.interval | Duration | 10s | 모든 타깃을 폴링하는 주기 (가상 스레드로 팬아웃). |
| ticker.poll.timeout | Duration | 5s | 체크 1회당 connect/read 타임아웃. |
| ticker.poll.failure-threshold | int | 3 | 연속으로 이만큼 실패해야 DOWN — 한 번의 순단으로 아무도 깨우지 않는 디바운스. |
| ticker.poll.degraded-latency-ms | long | 1000 | 성공했지만 이보다 느린 체크는 DEGRADED로 표시. |
| ticker.poll.staleness-multiplier | int | 3 | interval × 이 값 동안 폴링이 없으면 오래된 데이터를 믿지 않고 UNKNOWN으로 표시. |
ticker.targets — 정적 타깃
| 프로퍼티 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| ticker.targets | list | [] | 자가등록할 수 없는 것들(nginx, 외부 HTTP 엔드포인트). 항목: name, type (SPRING | HTTP), url, 선택 tags. |
| ticker.ui-targets-store-path | String | — | UI에서 추가한 모니터의 옵트인 파일 저장 (플랫 JSON — no-DB 원칙 유지). 미설정이면 UI 모니터는 재시작 시 사라집니다. |
ticker.alert.* — 알림 (전부 기본 꺼짐)
| 프로퍼티 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| ticker.alert.enabled | boolean | false | 알림 평가+발송의 마스터 스위치 — 장애 알림(🔴 DOWN / 🟢 복구)과 지표 임계 알림(⚠️). 꺼져 있어도 규칙 저장소와 /api/alerts/** 읽기·편집 API는 살아있어서(대시보드의 임계값 기반 심각도 색이 이걸 씁니다) 꺼둔 채 편집한 규칙이 나중에 켜면 그대로 적용됩니다. |
| ticker.alert.slack-webhook-url | String | — | Slack 인커밍 웹훅. 자격증명입니다 — ${SLACK_WEBHOOK_URL:} 패턴이나 TICKER_ALERT_SLACK_WEBHOOK_URL env로만, 절대 커밋하지 마세요. 빈 문자열은 미설정으로 취급. |
| ticker.alert.cooldown | Duration | 15m | 같은 장애를 다시 알리기까지의 최소 간격 (플랩 억제). |
| ticker.alert.metric-interval | Duration | 30s | 지표 임계 규칙을 살아있는 SPRING 타깃에 대해 평가하는 주기. |
ticker.history.* — 옵트인 지표 히스토리
| 프로퍼티 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| ticker.history.enabled | boolean | false | 5분~7일 범위 선택을 위한 지표 히스토리 영속화. 꺼져 있으면 완전 인메모리 — DB도 스키마도 없음. |
| ticker.history.db | enum | H2 | H2 (내장 파일, 무설정) | MYSQL | POSTGRESQL. |
| ticker.history.h2-path | String | ./data/ticker-history | H2 파일 경로 (db=H2일 때만). 프로덕션에선 내구성 있는 볼륨에. |
| ticker.history.url | String | — | JDBC URL — MySQL/PostgreSQL일 때 필수 (예: jdbc:mysql://host:3306/ticker). |
| ticker.history.username / password | String | — | DB 자격증명 — 환경변수로만, 절대 커밋 금지. |
| ticker.history.init-schema | boolean | true | 시작 시 스키마 자동 생성 (CREATE TABLE IF NOT EXISTS). DBA가 미리 만들어주는 환경이면 false로 — 번들 DDL: db/ticker-history-schema-<db>.sql. |
| ticker.history.sample-interval | Duration | 15s | 레코더가 화이트리스트 지표를 샘플링하는 주기. |
| ticker.history.retention | Duration | 7d | 이보다 오래된 샘플은 매시간 프루닝으로 삭제. |
| ticker.history.max-buckets | int | 240 | 범위 쿼리가 반환하는 다운샘플 포인트의 최대 개수. |
| ticker.history.archive.enabled | boolean | false | 삭제 전 아카이브: 오래된 행을 gzip CSV로 내보내고 검증한 뒤에야 지웁니다 — 실패한 내보내기는 재시도되고, 데이터가 그냥 사라지는 일은 없습니다. |
| ticker.history.archive.dir | String | ./data/ticker-history-archive | 아카이브 파일이 쌓이는 디렉터리. |
| ticker.history.archive.file-retention | Duration | 90d | 롤링 캡: 이보다 오래된 아카이브 파일 삭제 (Logback 스타일). |
| ticker.history.archive.max-total-size-mb | long | 0 (unlimited) | 롤링 캡: 아카이브 디렉터리를 이 크기 이하로 유지, 오래된 것부터 삭제. |
ticker.client.* — 감시 대상 앱
| 프로퍼티 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| ticker.client.enabled | boolean | true | 시작 시 컬렉터에 등록하고 하트비트로 유지. |
| ticker.client.collector-url | String | (required) | 컬렉터의 베이스 URL (예: http://ticker:8080) — base-path를 설정했다면 포함해서. |
| ticker.client.url | String | own http://<ip>:<port> | 컬렉터가 이 인스턴스를 폴링할 주소. 기본값(자기 자신의 주소)이 N대 레플리카가 설정 하나를 공유할 때 정답 — 각 파드가 자기 주소로 등록합니다. NAT/포트매핑/TLS일 때만 명시하세요. 레플리카들을 하나의 로드밸런서 URL로 가리키게 하면 안 됩니다. |
| ticker.client.name | String | spring.application.name | 벽에 표시되는 이름. 레플리카들은 이걸 공유합니다 — 벽이 이름으로 묶고, 인스턴스는 hostname:port로 구분. |
| ticker.client.type | enum | SPRING | SPRING (actuator health + 큐레이션 지표) | HTTP (GET, 2xx면 정상). |
| ticker.client.tags | list | [] | 타일에 표시되는 자유 태그 (팀, 환경 등). |
| ticker.client.heartbeat-interval | Duration | 30s | 주기적 재등록 — 컬렉터가 재시작해도 벽이 저절로 채워집니다. 0 이하로 비활성화. |
| ticker.client.deregister-on-shutdown | boolean | true | 정상 종료 시 벽에서 스스로 내려갑니다 — 롤링/블루그린 배포가 뒷정리를 해요(배포는 장애가 아님). 크래시는 이걸 건너뛰고 DOWN으로 남습니다 — 의도된 동작. |
| ticker.client.exclude-actuator-requests | boolean | true | /actuator 요청을 이 앱의 http.server.requests에서 제외 — 요청/초·지연·에러율이 컬렉터의 폴링이나 k8s 프로브가 아니라 진짜 트래픽을 보여줍니다. |
붙여넣기용 — 프로덕션스러운 컬렉터 설정 한 벌:
# a fuller collector config, everything optional
ticker:
server:
base-path: /ticker # UI → /ticker/, API → /ticker/api/**
public-url: https://ops.acme.com/ticker
registration-expiry: 10m # autoscaling ghost cleanup
poll:
interval: 10s
failure-threshold: 3
alert:
enabled: true
slack-webhook-url: ${SLACK_WEBHOOK_URL:}
history:
enabled: true
db: H2
h2-path: /var/lib/ticker/history
retention: 7d
archive:
enabled: true
dir: /var/lib/ticker/history-archiveSlack 알림
알림은 두 갈래입니다. 장애형 — 서비스가 죽거나(🔴) 되살아날 때(🟢). 지표 임계형 — CPU>80% 같은 규칙이 지속 시간을 채우면(⚠️). 인스턴스 단위로 식별되어 hostname:port와 IP가 함께 옵니다.
웹훅 연결 — 3단계
- 1api.slack.com/apps 에서 앱 하나 만들기 (From scratch, 워크스페이스 선택)
- 2Incoming Webhooks 켜고 → Add New Webhook to Workspace → 채널 선택. 웹훅 URL 하나가 채널 하나에 바인딩됩니다 — 채널을 바꾸려면 웹훅을 새로 만드세요.
- 3받은 URL을 환경변수로 넘기기 — yaml에 값을 적지 말고 ${SLACK_WEBHOOK_URL:} 패턴으로. 빈 값은 미설정으로 취급되어 알림이 로그로만 남습니다.
# application.yml
ticker:
server:
public-url: https://ops.acme.com/ticker # "Open Ticker board" link in alerts
alert:
enabled: true
slack-webhook-url: ${SLACK_WEBHOOK_URL:} # env-templated; blank = alerts stay log-inert
cooldown: 15m배포는 장애가 아니다
롤링/블루그린 배포 때마다 🔴가 오면 아무도 알림을 안 믿게 됩니다. 그래서 두 가지가 기본으로 준비돼 있어요. 첫째, 정상 종료하는 인스턴스는 스스로 등록을 해제합니다 (deregister-on-shutdown, 기본 on) — 내려가는 파드는 조용히 벽에서 사라져요. 크래시는 셧다운 훅을 안 타니 그대로 DOWN으로 남고요 — 그게 맞으니까. 둘째, 더 큰 작업엔 silence 창을 거세요:
# before a deploy / maintenance — suppress alerts for 10 minutes
curl -X POST http://ticker:8080/api/alerts/silence \
-H 'Content-Type: application/json' -d '{"minutes": 10}'
# check / lift it early
curl http://ticker:8080/api/alerts/silence
curl -X DELETE http://ticker:8080/api/alerts/silencesilence가 활성인 동안 발송은 억제되지만, 창이 끝날 때 아직 DOWN인 건 그때 알립니다. silence가 진짜 장애를 삼키는 일은 구조적으로 없습니다.
런타임 API
| GET · PUT /api/alerts/rules | 임계 규칙 읽기/수정 (threshold, forSeconds, cooldownSeconds, enabled) — UI의 🔔 팝오버와 같은 것. 기본 규칙: CPU>80%, 시스템 CPU>90%, 힙>85%, 디스크 여유<10%, GC 오버헤드>25%, 열린 파일>80%. |
| GET /api/alerts/recent | 최근 발화 이력 — Slack 없이도 UI에서 확인 가능. |
| POST · GET · DELETE /api/alerts/silence | 배포/점검 창: 활성 동안 발송이 억제되고, 창이 끝날 때 아직 DOWN인 것은 그때 알립니다 — silence가 진짜 장애를 삼킬 수 없게. |
레플리카 & 인스턴스 구분
실무 배포에서 인스턴스에 이름을 하나씩 붙이는 사람은 없습니다 — ASG, Beanstalk, k8s 모두 같은 설정으로 N대가 한꺼번에 뜨죠. Ticker는 이걸 전제로 설계됐습니다. 레플리카들은 같은 이름(spring.application.name)을 공유하고, 각 인스턴스는 등록 시점에 자기 hostname:port + IP로 스스로를 구분합니다. 설정 파일 하나를 N대가 그대로 공유해도 서로 덮어쓰지 않아요.
벽에서는 같은 이름이 타일 하나로 묶입니다 — 8/8 UP 같은 집계, 인스턴스별 점 스트립, 그리고 아픈 인스턴스가 항상 위로 올라오는 워스트-퍼스트 행. 타일을 열면 상세 헤더에 HOST / IP / URL 아이덴티티 라인과 인스턴스 스위처(6대 이하면 칩, 그 이상은 드롭다운)가 있어서 한 대씩 파고들 수 있습니다.

운영 노트
감시자를 감시하세요. 컬렉터가 죽으면 그 사실은 컬렉터 바깥에서 알아채야 합니다. 컬렉터도 자기 /actuator/health를 노출하니, k8s 라이브니스 프로브와 외부 핑 하나를 꼭 같이 걸어두세요. 그리고 Ticker는 기존 온콜 알림을 대체하는 게 아니라 그 옆에서 도는 라이브니스 보드입니다 — 검증되기 전까지는요.
히스토리 디스크: H2 파일은 보존 기간만큼 크다가 프루닝이 돌면 평평해집니다. 단, 행을 지워도 파일 자체는 줄지 않아요(공간을 재사용). 보존을 줄이고 디스크를 되찾으려면 앱을 멈추고 .mv.db를 지우면 됩니다 — 옵트인 히스토리라 다시 쌓이면 그만.
규모가 커지면 MySQL/PostgreSQL로 옮기고(자격증명은 env로만) 아카이브를 켜세요 — 오래된 행을 gzip CSV로 내보내 검증한 뒤에야 지웁니다. 정말 많다면 ts_millis 기준 RANGE 파티셔닝 후 파티션 DROP이 행 단위 삭제보다 훨씬 낫습니다.
게이트웨이 뒤라면 ticker.server.base-path: /ticker 한 줄로 UI와 API 전체가 접두사 밑으로 이동합니다. /actuator는 프로브 경로가 흔들리지 않게 제자리에 남고요. Slack 알림의 링크는 ticker.server.public-url이 결정합니다.