Clickhouse 통계 테이블 작성법
1. 개요
대용량 Observability 데이터(Logs, Traces, Metrics)를 직접 원본 테이블에서 실시간 분석하면 쿼리 비용이 매우 커지고 대시보드나 알림 시스템의 응답 속도가 저하됩니다. 이를 해결하기 위해 ClickHouse에서는 Materialized View를 활용한 통계 집계 테이블 구조를 적용할 수 있습니다.
대용량 Observability 데이터(Logs, Traces, Metrics)를 직접 원본 테이블에서 실시간 분석하면 쿼리 비용이 매우 커지고 대시보드나 알림 시스템의 응답 속도가 저하됩니다. 이를 해결하기 위해 ClickHouse에서는 Materialized View를 활용한 통계 집계 테이블 구조를 적용할 수 있습니다.
http://10.10.30.238:8980/98_personal/observability-all-in-one/-/tree/main/custom-grafana?ref_type=headsobservability-all-in-one/custom-grafana/이 문서는 Custom Grafana 이미지가 어떤 역할을 하는지, 어디를 수정해야 하는지, 어떻게 빌드/배포하는지를 새 담당자가 빠르게 파악할 수 있도록 정리한 인수인계용 가이드입니다.
세부 구조 설명은 custom-grafana/README.md 내용을 기반으로 하며, 운영상 자주 쓰는 관점 위주로 재구성했습니다.

OTLP Router는 OpenTelemetry Collector 앞단에 위치하여 수집되는 Telemetry 데이터를 서비스, 테넌트, 환경별로 라우팅(경로 지정) 해주는 역할을 수행합니다.
우리 시스템에서는 여러 테넌트에서 들어오는 OTLP gRPC/HTTP 요청을 자동으로 라우팅 하며, Trace/Metric/Log 데이터가 지정된 Collector로 전달될 수 있도록 구성되어 있습니다.
apiVersion: 1 # Grafana Alerting 설정 파일의 API 버전 (현재는 1이 표준)
groups: # 알림 규칙 그룹 목록
- orgId: 1 # 이 그룹이 속한 조직 ID (일반적으로 1)
name: 10s Evaluation Group # 알림 규칙 그룹 이름 (식별 용이성을 위해 설정 주기를 포함)
folder: alert # 규칙이 저장될 Grafana 폴더 이름 (대시보드 폴더와 연동됨)
interval: 10s # 이 그룹 내의 모든 규칙을 평가(Evaluation)하는 주기 (매우 짧은 주기로 설정)
rules: # 이 그룹에 포함된 개별 알림 규칙 목록
- uid: c72cd04e-d8f9-46d5-96f2-f8d908453338 # 규칙의 고유 ID (Grafana에서 자동 생성 또는 수동 지정)
title: CPU # 알림 규칙의 제목 (알림 발생 시 표시됨)
condition: C # 경보 상태를 결정하는 최종 데이터 쿼리/표현식의 RefID (여기서는 임계값 검증인 C)
data: # 규칙 평가에 사용되는 데이터 소스 쿼리 및 표현식 목록 (쿼리 A -> 표현식 B -> 임계값 C 순으로 처리)
# 1. ClickHouse 쿼리 (RefId: A)
- refId: A # 쿼리의 참조 ID
queryType: timeseries # 쿼리 유형 (시계열 데이터)
relativeTimeRange: # 쿼리할 데이터의 시간 범위 (현재 평가 시간 기준 상대적)
from: 300 # 현재 시간으로부터 300초(5분) 전
to: 0 # 현재 시간까지
datasourceUid: clickhouse # 데이터 소스 UID (ClickHouse 플러그인)
model: # 데이터 소스 쿼리 모델 상세 설정
datasource:
type: grafana-clickhouse-datasource
uid: clickhouse
editorType: sql # 쿼리 편집기 유형 (SQL)
format: 0
intervalMs: 300000 # 시계열 데이터의 최소 간격 (5분, 쿼리 내 bucket_s=10s와 다름, ClickHouse 쿼리 자체는 10초 버킷 사용)
maxDataPoints: 43200
meta:
builderOptions:
columns: []
database: ""
limit: 1000
mode: list
queryType: table
table: ""
pluginVersion: 4.7.0
queryType: timeseries
# ClickHouse SQL 쿼리 (Raw Query)
rawSql: "/* host.name 별 CPU 사용률(%) 시계열\r\n - value: (1 - avg(idle)) * 100\r\n - 그룹화: (t, host)\r\n*/\r\nWITH\r\n toDateTime($__fromTime) AS t_from,\r\n toDateTime($__toTime) AS t_to,\r\n 10 AS bucket_s,\r\n raw AS (\r\n SELECT\r\n toStartOfInterval(TimeUnix, toIntervalSecond(bucket_s)) AS t,\r\n TimeUnix AS ts,\r\n toString(multiIf(mapContains(ResourceAttributes, 'host.name'), ResourceAttributes['host.name'], mapContains(Attributes, 'host.name'), Attributes['host.name'], '')) AS host,\r\n toString(multiIf(mapContains(Attributes, 'state'), Attributes['state'], mapContains(ResourceAttributes, 'state'), ResourceAttributes['state'], '')) AS state,\r\n toFloat64(Value) AS v\r\n FROM otel_metrics_gauge\r\n PREWHERE TimeUnix BETWEEN t_from AND t_to\r\n WHERE MetricName = 'system.cpu.utilization'\r\n )\r\nSELECT\r\n t AS time,\r\n host,\r\n round((1 - avgIf(v, state = 'idle')) * 100, 1) AS cpu,\r\n formatDateTime(max(ts), '%Y-%m-%d %H:%i:%S', 'Asia/Seoul') AS occ_time\r\nFROM raw\r\nWHERE host != '' AND state != ''\r\nGROUP BY t, host\r\nORDER BY time ASC, host ASC;"
refId: A
# 2. 표현식 (RefId: B) - 쿼리 A의 결과를 집계
- refId: B # 표현식의 참조 ID
relativeTimeRange:
from: 300
to: 0
datasourceUid: __expr__ # Grafana 내장 표현식 엔진 사용
model:
conditions: # 이 표현식에서는 사용하지 않음 (대신 C에서 사용)
- evaluator:
params: []
type: gt
operator:
type: and
query:
params:
- B
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: A # 입력은 쿼리 A의 결과
intervalMs: 1000
maxDataPoints: 43200
reducer: last # A의 시계열 데이터 중 '가장 최근' 값만 가져옴 (호스트별 최신 CPU 사용률)
refId: B
settings:
mode: replaceNN # No-Data 처리 방식 (Null/NaN 값을 대체)
replaceWithValue: 0 # 대체 값 (0으로 설정하여 데이터가 없으면 0%로 간주)
type: reduce # 데이터 포인트를 단일 값으로 줄이는(Reduce) 표현식
# 3. 임계값 검증 (RefId: C) - 표현식 B의 결과에 임계값 적용
- refId: C # 임계값 표현식의 참조 ID (최종 경보 조건)
relativeTimeRange:
from: 300
to: 0
datasourceUid: __expr__
model:
conditions:
- evaluator: # 평가 조건
params:
- 70 # 임계값 (70%)
type: gt # Greater Than (초과)
operator:
type: and
query:
params:
- C
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: B # 입력은 표현식 B의 결과 (최신 CPU 사용률 값)
intervalMs: 1000
maxDataPoints: 43200
refId: C
type: threshold # 임계값을 적용하여 상태를 OK/Alerting으로 변경하는 표현식
dashboardUid: server-overview-tenant # 이 규칙이 참조하는 대시보드 UID
panelId: 112 # 이 규칙이 참조하는 대시보드 내 패널 ID
noDataState: OK # 쿼리 A에서 데이터가 없을 때의 상태 (OK로 설정하여 False Positive 방지)
execErrState: OK # 쿼리 실행 중 오류가 발생했을 때의 상태 (OK로 설정하여 False Positive 방지)
for: 0s # 경보 상태가 유지되어야 하는 최소 시간 (0s는 조건 충족 즉시 경보 발생)
annotations: # 알림 메시지에 포함될 추가 정보 (템플릿 변수 사용 가능)
__dashboardUid__: server-overview-tenant
__panelId__: "112"
description: |- # 알림 본문 (Markdown 형식)
---------------------------------------
서버 CPU 사용률이 임계치를 초과했습니다.
- 호스트: {{ $labels.host }} # 쿼리 A에서 추출된 레이블 (host)
- 발생 시간: {{ $labels.occ_time }} # 쿼리 A에서 추출된 레이블 (occ_time)
- 현재 CPU 사용률(%): {{ printf "%.1f" $values.B.Value }} % # 표현식 B의 최신 값
- 임계값(%): 70 %
---------------------------------------
labels: # Prometheus 스타일 레이블 (라우팅 및 필터링에 사용됨)
site: NIPA
isPaused: false # 규칙 활성화 여부 (false는 활성화 상태)
.otel/otel.properties의 otel.jmx.config=./jmx_config.yaml은 OpenTelemetry Java Agent(JMX Metric Insight 서브시스템)가 읽어야 할 규칙 파일을 지정합니다. 상대 경로는 otel.properties 위치를 기준으로 해석되므로 실제 파일은 /home/ryankimjh/monitoring/.otel/jmx_config.yaml입니다.otel.metric.export.interval, otel.metrics.exporter, otel.exporter.otlp.endpoint, otel.exporter.otlp.headers 등 일반 설정이 그대로 적용됩니다. 즉 JMX 메트릭도 http://otel.monitoring.mesimsaas.com로 전송되며, 리소스/서비스 속성(otel.service.name=API_GATEWAY_AGENT, tenant.id 등)과 instrumentation scope io.opentelemetry.jmx를 공유합니다.otel.jmx.discovery.delay(미설정 시 기본값 동작)는 첫 번째 MBean 탐색 이후 다음 탐색 사이 대기 시간을 밀리초 단위로 조정하여 동적 MBean 등록에도 지속적으로 대응합니다..otel/otel.properties를 읽어 OTLP/리소스/로그/트레이스 설정과 함께 JMX 설정을 읽어옵니다.otel.jmx.config에 지정된 하나 이상의 YAML 파일을 순서대로 파싱해 rules 블록을 빌드합니다.bean 또는 beans)과 속성(mapping)을 정의하며, Agent는 로컬 MBeanServer에 주기적으로 접근해 지정된 속성 값을 폴링합니다.metric, type(counter|updowncounter|gauge), unit, metricAttribute(태그) 정의에 따라 정규화되며, 필요 시 prefix, sourceUnit, dropNegativeValues 등이 반영됩니다.indigomq, camel, jetty, kafka-broker, tomcat, wildfly, hadoop, jvm 등 자주 쓰이는 런타임/프레임워크에 대한 사전 구성 번들을 제공합니다.-Dotel.jmx.target.system=jetty,kafka-broker 형태로 지정하면 해당 타깃의 표준 YAML 규칙이 자동 로드됩니다. 현재 환경은 커스텀 jmx_config.yaml만 사용하지만, 필요 시 타깃 번들과 병행하거나 참고할 수 있습니다.rules:
- bean: <ObjectName 또는 패턴>
metricAttribute:
<key>: param(<ObjectName-파라미터>)
<key>: beanattr(<MBean-속성>)
<key>: const(<고정값>)
prefix: <metric prefix>
unit: <기본 단위>
type: <기본 계측기 유형>
dropNegativeValues: <true|false>
mapping:
<MBeanAttribute>:
metric: <metric name>
type: <override type>
unit: <override unit>
desc: <설명>
sourceUnit: <원 단위>
dropNegativeValues: <true|false>
metricAttribute:
<key>: const(...)beans 배열을 사용하면 서로 다른 ObjectName 여러 개를 하나의 rule에 묶을 수 있습니다.prefix와 mapping의 metric 생략은 prefix + attribute 조합으로 자동 생성됩니다.HeapMemoryUsage.used처럼 attribute.field 구문으로 CompositeType 내부 필드를 지칭합니다.direction: in/out) 등 추가 태그만 바꿀 수도 있습니다. 단, 동일 메트릭을 재사용할 때 type, unit, desc는 완전히 일치해야 합니다.metricAttribute는 rule 단위 기본 태그와 metric 단위 추가 태그를 모두 허용합니다.param(name)은 ObjectName 파라미터(name=, context= 등)를 사용해 동적 태그를 생성합니다.beanattr(Type)는 MBean의 다른 속성 값을 태그로 노출합니다.const(sent)는 고정 문자열 태그를 지정할 때 사용합니다.type: state를 사용하고, 상태 매핑(tomcat.connector.state: { ok: STARTED, failed: [STOPPED,FAILED], degraded: '*' })을 정의하면 상태별 updowncounter(0/1)가 자동 생성됩니다.lowercase(...) 변환만 지원하며, 예를 들어 jvm.memory.type 태그를 HEAP/NON_HEAP → heap/non_heap으로 정규화할 수 있습니다.sourceUnit과 unit을 동시에 지정하면 ms→s, ns→s 등 자동 변환이 적용됩니다. 변환이 불가하면 에이전트가 에러 로그를 남깁니다.dropNegativeValues: true를 rule/metric 수준에서 설정하여, JVM이 “지원 안 함”을 의미하기 위해 반환하는 음수 값을 무시할 수 있습니다.otel.jmx.discovery.delay로 첫 탐색 이후 대기 시간을 제어합니다. MBean 탐색은 영속적으로 반복되며, Agent가 필요에 따라 주기를 동적으로 늘려 과부하를 방지합니다./home/ryankimjh/monitoring/.otel/jmx_config.yaml에 새로운 bean 블록을 추가하거나 다른 YAML을 작성해 otel.jmx.config=file1.yaml,file2.yaml처럼 다중 지정할 수 있습니다.otel.jmx.target.system으로 프레임워크별 번들을 활성화하고, 현재 커스텀 규칙과 병합해 사용할 수 있습니다.io.opentelemetry.jmx 관련 메시지를 확인해 YAML 파싱 오류가 없는지 점검합니다.camel.*, http.server.tomcat.*, indigomq.* 메트릭이 들어오는지 쿼리합니다.java -javaagent:./opentelemetry-javaagent.jar \
-Dotel.jmx.config=./jmx_config.yaml \
-Dotel.exporter.otlp.endpoint=http://<IP>:<PORT> \
-jar myapp.jar필요 시 -Dotel.jmx.target.system=tomcat,camel 또는 -Dotel.jmx.discovery.delay=2000 등을 추가해 사전 정의 메트릭과 탐색 주기를 세밀하게 제어할 수 있습니다.
k6는 서버·API·시스템의 부하 테스트, 스트레스 테스트, 성능 검증을 수행하기 위한 오픈소스 도구입니다. 트래픽을 코드 기반으로 시뮬레이션할 수 있어 이해하고 유지하기 쉬운 테스트 환경을 제공합니다.