본문으로 건너뛰기

Grafana로 시각화와 트렌딩

📍 현재 위치: 3부, 우리가 그동안 저장해 온 맥락화된(contextualized) 배치(batch) 데이터가 마침내 운영자가 지켜보고 엔지니어가 신뢰하는 하나의 그림이 되는 계층입니다.

쉽게 말하면

여러분의 히스토리언(historian)과 배치 데이터베이스를, 숫자들이 완벽하게 정리된 거대한 도서관이라고 생각해 보세요. Grafana는 그 도서관의 열람실입니다. 단 한 권의 책도 직접 소유하지 않습니다. 올바른 서가로 걸어가, 여러분이 요청한 행(row)들을 꺼내 와, 한눈에 읽을 수 있는 트렌드(trend)로 펼쳐 보일 뿐입니다. 중요한 부분은 대부분의 사람들이 잊는 바로 그것입니다. 모든 차트는 그저 저장된 질문(saved question) 이라는 점입니다. 데이터가 바뀌면 차트도 바뀝니다. 바로 그렇기 때문에 Grafana 트렌드는 증거가 될 수 있지만 그 스크린샷은 증거가 될 수 없습니다. 스크린샷은 책이 아니라 열람실을 찍은 사진이기 때문입니다.

이 장에서 다루는 내용

우리는 이미 들여다볼 가치가 있는 데이터를 갖고 있습니다. TimescaleDB 히스토리언 안의 14일 유가식(fed-batch) CHO 추적선(trace), PostgreSQL 안의 ISA-88/95 배치 모델, 그리고 그 둘을 조인(join)하는 맥락화 뷰(view)입니다. 쉽게 말하면, 히스토리언 은 모든 센서 판독값을 담고 있는 시계열 데이터베이스이고, 배치 모델 은 각 배치가 무엇을 하고 있었는지를 담은 테이블이며, 맥락화 뷰 는 그 원시 숫자들을 그 배치 맥락에 조인합니다. (CHO는 항체를 만드는 중국 햄스터 난소(Chinese-hamster-ovary) 세포주입니다 — 1–2권 참조. ISA-88/95는 배치와 공장을 모델링하는 표준입니다.) 이 장에서는 이 모든 것 위에 오픈소스 대시보드 계층을 세웁니다.

우리는 (1) 공유 compose.yaml의 고정된 한 줄에서 Grafana를 띄우고, (2) 그것의 데이터 소스(data source)와 대시보드를 클릭으로 짜 맞추는 대신 코드로(as code) 프로비저닝(provisioning)하고, (3) 같은 맥락화 데이터 위에 운영자 뷰(operator view)와 더 조밀한 엔지니어 뷰(engineer view)를 만들고, (4) 알림 규칙(alert rule) 하나를 연결하고, (5) Grafana를 규제 공장에 어른스러운(grown-up) 선택으로 만드는 두 가지 — 그것의 AGPLv3 라이선스 조항과, 트렌드는 검증된 데이터로부터 재현 가능할 때에만 증거가 된다는 엄격한 규칙 — 과 정면으로 마주합니다. Grafana는 쉽게 말해 데이터베이스로부터 차트를 그리는 도구입니다 — AVEVA PI Vision 같은 상업용 공정 시각화 제품의 오픈소스 대응물이며, 맥락화된 실시간 데이터 위의 운영자/엔지니어 대시보드는 단일클론항체(monoclonal antibody, mAb) 라인을 실제로 운전하는 데 핵심입니다 [1]. (이 장 전체에서 "규제 공장(regulated plant)"과 "GxP"는 의약품이 어떻게 만들어지고 기록되는지를 규율하는 Good-x-Practice 규칙 — GMP, GLP 등 — 에 묶인 제약 시설을 뜻합니다.)

Grafana는 이미 스택에 들어 있다

이 책에서 여러분은 Grafana를 설치하지 않습니다. 그것은 공유 플랫폼 스택에서 단 한 번 선언되었으며, Postgres 및 MQTT 메시지 브로커(Mosquitto — 포착 계층이 센서 판독값을 발행하는 메시지 버스)와 나란히 core 프로파일과 함께 올라옵니다. CHO 시뮬레이터는 core 서비스가 아니라 make data로 실행되는 별도의 Python 패키지입니다. 다음은 examples/platform/compose/compose.yaml에서 가져온 실제 서비스입니다.

# examples/platform/compose/compose.yaml
grafana:
image: grafana/grafana-oss:11.4.0
profiles: ["core"]
<<: *restart
ports: ["3000:3000"]
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ../dashboards/provisioning:/etc/grafana/provisioning:ro
- grafana:/var/lib/grafana
depends_on:
postgres:
condition: service_healthy

이 블록은 천천히 읽으세요. 거의 모든 줄이 의도적인, GxP 색채를 띤 선택이기 때문입니다.

  • grafana/grafana-oss:11.4.0 — 태그로 고정되어 있고, 그에 대응하는 매니페스트 다이제스트(digest)는 examples/platform/versions.lock(make lock으로 재생성)에 기록되어 있으며, 거기서 grafana-oss:11.4.0 줄이 자신의 sha256:(정확한 이미지 바이트의 콘텐츠 지문(content fingerprint). 다른 이미지는 다른 지문을 계산하므로, 그 밖의 무언가를 돌려준 풀(pull)은 거부됩니다)을 담고 있습니다 — 따라서 docker compose pull이 검증된 대시보드 아래에서 이미지를 조용히 갈아치우는 일은 결코 일어날 수 없습니다. 이것은 Grafana Enterprise나 Grafana Cloud가 아니라 오픈소스 배포판이며, 고정된 11.4.x가 바로 저장소가 실제로 돌리는 것입니다.
  • profiles: ["core"] — Grafana는 항상 켜져 있는 기반입니다. compose 헤더에 따르면 core 프로파일은 저장/시각화 장들(1-2, 4-6, 16–18장)을 포괄하며, 포착·시맨틱·분석 장들도 모두 이를 통해 읽어 들입니다.
  • GF_USERS_ALLOW_SIGN_UP: "false" — 익명 자가 등록(self-registration)이 꺼져 있습니다. 규제 맥락에서는 모든 행위가 이름이 부여되고 프로비저닝된 신원에 귀속되기를 바라지, 즉석에서 만든 계정에 귀속되기를 바라지 않습니다.
  • ../dashboards/provisioning:/etc/grafana/provisioning:ro — 전체 프로비저닝 트리가 읽기 전용(read-only) 으로 마운트됩니다. Grafana는 시작 시 버전 관리되는 파일들에서 데이터 소스와 대시보드를 읽으며, 그것들에 되써넣을 수 없습니다.
  • depends_on: postgres … service_healthy — Grafana는 히스토리언과 배치 모델을 모두 담고 있는 데이터베이스가 헬스체크(healthcheck)를 통과하기 전까지는 아예 시작조차 하지 않습니다. 아직 준비되지 않은 데이터베이스를 가리키는 대시보드란 없습니다.

이를 띄우고 http://localhost:3000에서 접속합니다.

docker compose --profile core up -d

그 태그에 관해 이 책이 여러분에게 빚진 한 가지 메모입니다. 플랫폼의 사양 서술(spec narrative)은 더 새로운 Grafana를 겨냥하지만, 테스트된 compose 파일은 11.4.0을 고정하므로, 그것이 저장소가 실제로 돌리는 것이자 이 장이 인쇄하는 것입니다. 숫자 자체보다 중요한 것은 규율입니다. 다이제스트 수준 잠금(커밋된 versions.lock, make lock으로 재생성)의 핵심은, 그것이 기록하는 것이 CI가 받아 오는 것, 라이선스 인벤토리가 기록하는 것, 공급자 등록부(supplier register)가 검증하는 것과 정확히 일치하도록 하는 것입니다. 가동 중인 스택과 그 문서가 서로 어긋나도록 내버려 두어서는 안 됩니다.

클릭이 아니라 코드로 만드는 대시보드

규제 대상 대시보드를 잃는 가장 빠른 방법은, 브라우저에서 손으로 아름답게 만들어 놓고 어디에도 저장하지 않는 것입니다. Grafana의 답은 프로비저닝 입니다. 데이터 소스와 대시보드를 UI에서 클릭으로 짜 맞추는 대신, 시작 시 로드되는 버전 관리된 YAML/JSON 파일로 정의하는 것입니다 [2].

프로비저닝 트리, 파일별로

위 compose 서비스가 읽기 전용으로 마운트하고 저장소에 커밋한 그 provisioning/ 디렉터리는 고정된 형태를 갖습니다.

examples/platform/dashboards/provisioning/
├── datasources/
│ └── timescaledb.yaml # how Grafana reaches Postgres/TimescaleDB
├── dashboards/
│ ├── dashboards.yaml # tells Grafana where to find dashboard JSON
│ └── json/
│ └── br101-batch-overlay.json # the engineer/operator dashboard
└── alerting/
└── batch-alerts.yaml # alert rules + contact points as code

데이터 소스는 한 파일, datasources/timescaledb.yaml입니다(히스토리언과 배치 모델은 동일한 PostgreSQL 인스턴스이므로, 하나의 데이터 소스가 둘 다를 담당합니다).

# examples/platform/dashboards/provisioning/datasources/timescaledb.yaml
apiVersion: 1
datasources:
- name: TimescaleDB
uid: timescaledb # stable uid so dashboard JSON references survive
type: postgres
url: postgres:5432 # the compose service hostname, not localhost
user: ${POSTGRES_USER}
jsonData:
database: ${POSTGRES_DB}
sslmode: disable # fine inside the compose network; TLS is Ch 28
timescaledb: true # enables Grafana's TimescaleDB-aware $__timeGroupAlias / time_bucket handling
secureJsonData:
password: ${POSTGRES_PASSWORD}
editable: false # provisioned, read-only: changes go through Git

두 가지 세부가 제 몫을 합니다. uid: timescaledb안정적인(stable) 식별자입니다. 대시보드 JSON은 자동 생성된 식별자가 아니라 이 uid를 참조하므로, 노트북에서 내보낸 대시보드가 서버에서 변경 없이 로드됩니다. 그리고 editable: false는 운영자가 데이터 소스를 다른 데이터베이스로 조용히 다시 가리키게 할 수 없다는 뜻입니다. 그 변경은 파일을 거쳐야 하고, 그 파일은 Git 리뷰를 거칩니다.

대시보드 제공자(provider) 파일은 단지 Grafana를 대시보드 JSON 폴더로 가리킵니다.

# examples/platform/dashboards/provisioning/dashboards/dashboards.yaml
apiVersion: 1
providers:
- name: bioproc-dashboards
type: file
allowUiUpdates: false # the UI cannot overwrite the file-of-record
options:
path: /etc/grafana/provisioning/dashboards/json
foldersFromFilesStructure: true

allowUiUpdates: false는 규제적 태세입니다. 누군가 브라우저에서 패널(panel)을 탐색하고 손볼 수는 있지만, 디스크 위의 파일 — 버전 관리 안에 있는 그 파일 — 이 진실의 기록(record of truth)으로 남습니다. 실제 공장에서 Grafana가 권장하는 경로는 한 걸음 더 나아갑니다. 이 파일들을 CI/CD와 Git Sync를 통해 관리하여, 모든 대시보드 변경이 애플리케이션 코드와 똑같이 리뷰되고, 버전이 매겨지고, 재현 가능하게 배포되도록 하는 것입니다 [3].

Grafana 패널의 해부: 저장된 질문을, 필드 하나하나

대시보드는 JSON 문서이며, 그 안의 패널 은 우리 히스토리언에 대한 저장된 SQL 쿼리입니다. 커밋된 dashboards/json/br101-batch-overlay.jsonuid, title, tags, 대시보드 변수들의 templating 리스트, 그리고 panels 배열을 담습니다. 다음은 그 파일의 핵심 — 바이오리액터 온도 트렌드(그 측정된 공정값(process value, PV) 대 레시피 설정점(setpoint))를 시간으로 버킷화(time-bucketed)하여 그리는 패널입니다. SQL은 각 태그의 판독값을 1분 그룹으로 평균 내므로, 차트는 원시 샘플마다 점 하나를 찍는 대신 더 적고 더 매끄러운 점들로 렌더링됩니다. 각 태그는 업스트림 바이오리액터<asset>.<measurement>.<role> 규약을 따릅니다 — 여기서 BR101은 바이오리액터 유닛, Temp는 측정 항목, .PV는 역할(공정값, 즉 측정된 판독값으로, 설정점인 .SP와 대비됨)입니다.

{
"id": 1,
"title": "BR101 Temperature (PV vs setpoint)",
"type": "timeseries",
"datasource": { "type": "postgres", "uid": "timescaledb" },
"fieldConfig": { "defaults": { "unit": "celsius" } },
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 0 },
"targets": [
{
"refId": "A",
"rawSql": "SELECT time_bucket('1 minute', ts) AS time, avg(value) AS \"Temp PV\" FROM ts.sensor_reading WHERE tag = 'BR101.Temp.PV' AND batch_id = '$batch' AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1",
"format": "time_series"
}
]
}

패널을 필드 하나하나 따라가면, 패널은 저장된 질문이다 라는 추상적 주장이 구체적이 됩니다. typetimeseries이고, datasource는 안정적인 uid(timescaledb)로만 지명되므로 패널이 기계 사이를 변경 없이 옮겨 다닙니다. fieldConfig.defaults.unitcelsius이므로, y축은 맨숫자가 아니라 실제 공학 단위를 지닙니다. templating 리스트는 $batch 변수 — 그 자체가 쿼리(SELECT DISTINCT batch_id FROM ts.sensor_reading …)인 — 를 담으며, 이것이 운영자의 드롭다운을 채웁니다. 그리고 핵심을 짊어지는 부분은 targets[0]입니다. refId, time_seriesformat, 그리고 rawSql 문자열. 그 문자열이 전부의 요점입니다.

br101-batch-overlay.json의 패널 하나를 필드별로 해부하는 신원 카드: 대시보드 uid, 패널 title, tags, timescaledb 데이터 소스, celsius 단위, templating $batch 쿼리 변수, 그리고 time_bucket과 $batch·timeFilter 매크로, refId A, format time_series를 담은 강조된 target. 저장된 패널의 모든 필드는 하나의 질문에 관한 메타데이터이고, 유일한 페이로드는 target 안의 SQL이며, 그것은 패널이 렌더링될 때마다 살아 있는 데이터에 대해 다시 실행됩니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

이 패널은 하나의 질문에 불과합니다. 선택된 배치에 대해, 온도 태그를 분 단위로 버킷화하여 평균을 내라. $batch는 운영자가 드롭다운에서 고르는 대시보드 변수이고, $__timeFilter(ts)는 보이는 시간 범위를 주입하는 Grafana 매크로(macro)입니다. (GxP 태세를 위해서는 원시 '$batch' 보간 대신 Grafana의 값-이스케이프 ${batch:sqlstring}을 선호하세요. 그러면 변수가 SQL을 결코 변형할 수 없습니다 — 여기서는 $batch가 제한된 SELECT DISTINCT에서 오므로 블래스트 반경이 작지만, 그것을 이스케이프하는 것이 규율 있는 기본값입니다.) 일주일 뒤 같은 히스토리언에 이를 겨냥하면 같은 선이 나옵니다. 그 선은 데이터로부터 계산되는 것이지, 그 위에 붙여 놓은 것이 아니기 때문입니다. JSON 안의 어떤 것도 트렌드의 픽셀이 아닙니다 — 답은 결코 저장되지 않고, 질문만 저장됩니다.

흐름도: 버전 관리되는 세 개의 Git 파일(데이터 소스, 대시보드, 알림)이 Grafana 11.4.0에 읽기 전용으로 마운트되고, Grafana가 uid timescaledb를 통해 PostgreSQL과 TimescaleDB 히스토리언에 쿼리하여 행을 돌려받은 뒤, 운영자 뷰, 엔지니어 뷰, 그리고 연락 지점으로 라우팅되는 알림 규칙을 렌더링하는 모습.

두 청중, 하나의 데이터 집합

같은 맥락화 데이터가 아주 다른 두 독자를 섬기며, 좋은 관행은 그들에게 빽빽하게 절충한 하나가 아니라 두 개의 대시보드를 주는 것입니다.

운영자 뷰 대 엔지니어 뷰

운영자 뷰 는 차분하고 결정 지향적입니다. 진정으로 온라인 인 태그들 — 인라인 프로브(in-line probe)로 연속 측정되는 — 위의 큼직한 스탯(stat) 패널 몇 개(현재 역가(titer), DO, pH, 온라인 포도당(online glucose)), 온도 트렌드(생존 세포 밀도(viable-cell density)는 QC 실험실에서 간헐적으로 측정되어 lab.result에서 하루 두 번 조인되는 오프라인 벤치 결과이지 온라인 히스토리언 태그가 아니므로, 라이브 운영자 스탯이 아니라 엔지니어 뷰에 속합니다 — 온라인/인라인 대 오프라인의 구분은 시드 트레인과 세포 배양 오프라인 분석에서, 바이오리액터 태그 자체는 업스트림 바이오리액터에서 구축됩니다), 그리고 "밴드 안 / 밴드 밖(in band / out of band)"을 분명히 나타내는 빨강/초록 상태입니다. 위의 쿼리는 이미 각 값에 batch_id를 태그하므로, 운영자의 드롭다운 — 위 패널 해부에 나온 $batch 변수 — 은 지금 현장에 있는 배치로 모든 것을 필터링합니다. 운영자는 흘끗 보고 떠날 수 있어야 합니다. 대시보드의 임무는 "이 배치는 제어되고 있는가?"를 1초 안에 답할 수 있게 만드는 것입니다.

엔지니어 뷰 는 조밀하고 탐사적입니다. 모든 태그가 겹쳐지고, 시뮬레이터가 의도적으로 심은 7일째 0.5 °C 일탈(excursion), 배치 모델의 볼루스 피드(bolus-feed) 이벤트가 주석(annotation)으로 그려지고, events.operation_event에서 끌어온 Protein A 크로마토그램(chromatogram) 단계(phase, 다운스트림 크로마토그래피에서 구축됨)가 표시됩니다 — 예를 들어, 포착 용리(elution) 단계(pH가 ~3.3으로 떨어지는 UV280의 날카로운 피크)를 업스트림 역가 트렌드에 겹쳐 놓는 식입니다. 두 대시보드 모두 맥락화된 계층 — 히스토리언이 ISA-88/95 배치 모델에 조인된 것 — 을 읽기 때문에, 엔지니어는 "역가"와 "피드 이벤트"를 겹쳐 놓고 원인을 결과 옆에서 볼 수 있습니다. 커밋된 br101-batch-overlay.json은 바로 이 독자를 위해 engineer 태그를 답니다.

두 대시보드가 하나보다 나은 이유는, 두 독자가 서로 다른 박자로 서로 다른 질문을 던지기 때문이고, 빽빽하게 절충한 하나의 보드는 둘 다를 제대로 섬기지 못하기 때문입니다. 운영자는 겹쳐진 추적선에 빠져 허우적대고, 엔지니어는 한눈에 보기 위한 레이아웃에 태그 간 맥락을 빼앗깁니다. 아래의 데이터는 동일합니다 — 같은 맥락화 히스토리언 — 따라서 를 나누는 비용은 같은 프로비저닝 제공자 아래의 두 번째 JSON 파일 하나뿐입니다.

quality 열: Good으로 도착하지 않은 샘플을 회색 처리하기

다음은 우리 패널이 실제로 그리는 종류의 행으로, 히스토리언 하이퍼테이블(hypertable)에서 바로 가져온 것입니다(롱 포맷(long format), 타임스탬프마다 태그당 한 행).

ts | tag | value | unit | quality | batch_id
-------------------+-----------------+-------+------+---------+---------------
2026-05-08 07:00 | BR101.Temp.PV | 37.01 | degC | 192 | BATCH-2026-001
2026-05-08 07:00 | BR101.pH.PV | 7.05 | pH | 192 | BATCH-2026-001
2026-05-08 07:00 | BR101.DO.PV | 39.4 | %sat | 192 | BATCH-2026-001
2026-05-08 07:00 | BR101.Titer.PV | 2.41 | g/L | 192 | BATCH-2026-001
2026-05-08 07:01 | BR101.DO.PV | 38.7 | %sat | 64 | BATCH-2026-001

quality 열은 장식이 아닙니다. 그것은 엣지(edge)에서 포착된 품질 코드를 담고 있으며, 히스토리언 DDL — 테이블을 생성하는 데이터 정의 언어(Data Definition Language) 파일(examples/platform/db/20-historian.sql) — 은 그 인코딩을 주석에 고정합니다. 192 = Good, 64 = Uncertain, 0 = Bad 입니다. 연결: OPC UA & MQTT에서 구축했듯이, 그 192/64/0 값들은 Sparkplug 브리지가 자주 그대로 통과시키는 레거시 OPC DA(Classic) 코드이지, OPC UA 네이티브 품질이 아닙니다 — OPC UA는 완전히 별개의 인코딩 체계입니다(거기서 Good은 StatusCode 0이고, 나쁨은 상위 비트에 담깁니다). 따라서 같은 숫자 0은 두 체계에서 정반대를 뜻합니다 — 이 열이 쓰는 OPC DA 코드에서는 Bad, OPC UA에서는 Good — 바로 그렇기 때문에 히스토리언이 한 가지 규약을 고정하는 것이며, 여기서는 OPC DA 의미(0 = Bad)만 적용됩니다. 위 행들 중 넷은 Good (192)로 도착했고, 마지막 DO 샘플은 Uncertain (64)로 돌아왔습니다. 따라서 패널은 Good으로 도착하지 않은 값을 회색 처리하거나 표시할 수 있습니다. 대시보드는 신뢰할 수 없는 점을 마치 건전한 것처럼 조용히 트렌딩하지 않습니다. 심어진 데이터셋에서 이 Uncertain DO 판독값들은 무작위로 흩어져 있지 않습니다. 시뮬레이터는 7일째 냉각 일탈 구간에서만 그 플래그를 설정하는데, 그 동요(upset) 동안 프로브 판독값이 신뢰할 수 없게 되기 때문입니다 — 품질 플래그와 실제 공정 이벤트 사이의 가르칠 만한 연결고리입니다.

버전 관리되는 YAML과 JSON 프로비저닝 파일이 Grafana에 읽기 전용으로 마운트되고, Grafana가 결합된 PostgreSQL과 TimescaleDB 히스토리언에 SQL을 발행하며, 같은 맥락화 데이터가 차분한 운영자 대시보드, 조밀한 엔지니어 대시보드, 그리고 연락 지점으로 라우팅되는 알림으로 렌더링되는 모습을 보여주는 계층 다이어그램. Grafana는 어떤 데이터도 소유하지 않습니다. 코드로 프로비저닝된 대시보드와 알림이 필요할 때마다 맥락화 히스토리언을 쿼리하므로, 모든 트렌드는 저장된 그림이 아니라 재현 가능한 질문입니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

알림: 여러분을 호출하는 대시보드

새벽 3시에 아무도 보지 않는 트렌드는 그다지 쓸모가 없습니다. Grafana의 통합 알림(unified alerting)은 하나 이상의 데이터 소스에 걸쳐 규칙을 평가하고, 연락 지점(contact point)알림 정책(notification policy) 을 통해 알림을 라우팅합니다 — PI Vision/Notifications의 오픈소스 대응물입니다 [4]. 규칙도 대시보드처럼 코드로 프로비저닝됩니다.

코드로 프로비저닝된 알림 규칙의 해부: 쿼리 더하기 임계값

심어진 7일째 온도 일탈에 대한, 커밋된 alerting/batch-alerts.yaml 규칙은 다음과 같습니다.

# examples/platform/dashboards/provisioning/alerting/batch-alerts.yaml
apiVersion: 1
groups:
- orgId: 1
name: bioreactor
folder: BR101
interval: 1m
rules:
- title: BR101 temperature out of band
condition: C
data:
- refId: A # query the historian
datasourceUid: timescaledb
model:
rawSql: >
SELECT $__timeGroupAlias(ts,'1m'), avg(value) AS temp
FROM ts.sensor_reading
WHERE tag='BR101.Temp.PV' AND $__timeFilter(ts)
GROUP BY 1 ORDER BY 1
- refId: C # threshold: outside 36.5-37.5 degC
type: threshold
model:
conditions:
- evaluator: { type: outside_range, params: [36.5, 37.5] }
for: 5m # must persist 5 min to avoid flapping
labels: { severity: warning }

규칙은 두 단계로 읽히며, 필드 하나하나의 해부가 그 메커니즘을 분명히 드러냅니다. 그룹은 어디서얼마나 자주 를 고정합니다. orgId: 1, name: bioreactor, folder: BR101, interval: 1m. 규칙 자체는 condition: C를 설정하며, 이는 발화 여부를 결정하는 데이터 노드를 지명합니다. 그다음 data가 두 단계를 담습니다. refId: A는 히스토리언 쿼리(ts.sensor_reading에 대한 rawSql, 1분 버킷으로 시간 그룹화)이고, refId: Cevaluatoroutside_range이며 params: [36.5, 37.5]threshold입니다. A가 C를 먹이고, C가 결정합니다. for: 5m 절은 안티플랩(anti-flap) 밸브로 — 순간적인 잡음에 규칙이 "플래핑(flapping)", 즉 되풀이하여 발화하고 해제되는 것을 막아 줍니다 — labels: { severity: warning }는 알림 정책이 라우팅하는 키입니다.

batch-alerts.yaml의 규칙 하나를 필드별로 해부하는 신원 카드: apiVersion, orgId, 그룹 이름 bioreactor, 폴더 BR101, interval 1m, condition C, 그리고 refId A가 히스토리언을 쿼리하여 refId C를 먹이는 두 단계 데이터 모델, params 36.5에서 37.5인 outside_range 임계값 evaluator, 더하기 for 5m 안티플랩 절과 severity warning 레이블. 알림 규칙은 히스토리언 쿼리(refId A)가 임계값 테스트(refId C)에 연결된 것이며, 두 절반 모두 Git에 살아 있으므로 일탈 조사가 무엇이 왜 발화했는지를 정확히 다시 평가할 수 있습니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

for: 5m 절은 유용한 알림과 성가신 알림을 가르는 차이입니다. 잡음이 섞인 단일 샘플 하나는 누구도 호출하지 않지만, 진짜 5분짜리 드리프트(drift)는 호출합니다. 이 데모에 관한 한 가지 정직한 단서: 심어진 일탈은 하한값(36.5 °C) 바로 위에 앉도록 설계되어 있으므로, 이 규칙은 의도적으로 플랩(flap)할 수 있는 경계 근접 사례입니다. 실제 공장에서는 경보 밴드가 제어 밴드 안쪽에 넉넉히 자리하므로(예: 하한 전용 lt 36.8), 0.5도 하강이 결정적으로 그것을 벗어납니다. 이 알림이 아닌 것에 주목하세요. 그것은 저장된 판단(stored judgment)이 아닙니다. 그것은 쿼리 더하기 임계값(threshold)이며, 둘 다 Git에 있고, 둘 다 다시 평가할 수 있습니다. 나중에 일탈 조사(deviation investigation)가 "무엇이, 왜 발화했는가"를 묻는다면, 답은 기억이 아니라 규칙 정의와 그것이 실행된 히스토리언 행들입니다.

왜 중요한가

이 장의 가장 깊은 발상은 한 줄입니다. 트렌드는 검증된 데이터로부터 재현 가능할 때에만 증거이고, 스크린샷은 기록이 아니다. 세 규제기관이 저마다의 말로 같은 것을 이야기합니다 — 미국 FDA(21 CFR Part 11과 그 데이터 무결성 Q&A), EU(Annex 11), 그리고 영국 MHRA(그 GxP 데이터 무결성 지침으로, 기록이 귀속 가능하고(Attributable), 읽을 수 있고(Legible), 동시적이고(Contemporaneous), 원본이며(Original), 정확해야(Accurate) 한다는 ALCOA+ 원칙의 출처입니다). Part 11은 시스템이 정확하고 완전한 기록 사본을 생성하고 타임스탬프가 찍힌 감사 추적(audit trail)을 유지하기를 요구합니다(구체적으로 11.10(b)와 11.10(e)) [5]. EU Annex 11은 전산화 시스템의 인쇄물과 트렌드가 명확해야 하며, 어떤 보고서든 그 바탕이 되는 데이터가 시스템까지 추적 가능해야 한다고 요구합니다 [6]. FDA의 데이터 무결성 Q&A와 MHRA의 ALCOA+ 지침은 둘 다 무결성 측면에서 같은 요점을 짚습니다. GMP 결정은 일시적인 디스플레이가 아니라, 귀속 가능하고(attributable), 원본이거나 인증 사본이며(original-or-certified-copy), 재구성 가능한(reconstructable) 기록에 근거해야 한다는 것입니다 [7][8].

Grafana는 올바르게 사용한다면 이에 아름답게 들어맞습니다. 모든 패널이 히스토리언에 대한 저장된 쿼리이지 — 구워 넣은 이미지가 아니므로 — 접근 권한이 있는 누구라도 필요할 때마다 그것을 다시 실행하여 동일한 트렌드를 재생성할 수 있습니다. 그것이 실무에서 "데이터로부터 재현 가능"이 뜻하는 바입니다. 실패 양상은 PNG를 보고서에 붙여 넣는 문화입니다. 그 그림이 그것을 만들어낸 행들과의 연결을 모두 잃은 채로 살아남는 것입니다. 코드로 만드는 대시보드는 좋은 경로를 강화합니다. 출하 트렌드를 그린 정확한 그 쿼리가, 여러분이 diff하고, 리뷰하고, 재현할 수 있는 버전 관리된 산출물(versioned artifact)이 됩니다.

스크린샷이 실사를 통과하지 못하는 이유

이는 취향의 문제가 아니라, 실제 데이터 무결성 지적이 떨어지는 지점입니다. MHRA의 GXP 데이터 무결성 지침은 GMP 결정이 원본(original) 기록(또는 진실되고 검증 가능한 사본)에 근거해야 하며, 그 바탕이 되는 데이터와 메타데이터를 더는 재구성할 수 없는 경우 동적인 전자 데이터의 정적인 인쇄물이나 디스플레이는 그 자체로 허용 가능한 기록이 아니라고 명시합니다 [8]. FDA의 데이터 무결성 Q&A는 CGMP 측면에서 같은 요점을 짚습니다. 전자 기록과 그 감사 추적은 보존되고 검토 가능해야 하며, 일시적인 디스플레이나 붙여 넣은 이미지는 그 배후의 귀속 가능하고, 원본이며, 재구성 가능한 기록을 대체하지 못합니다 [7]. 이를 Grafana 세계로 옮기면 규칙은 구체적입니다. 보고서에 떨어뜨린 출하 트렌드의 PNG는 재현 불가능한 인쇄물입니다 — 그것을 만들어낸 행들, 그 quality 플래그, 그리고 정확한 그 쿼리가 모두 그 그림에서 잘려 나갑니다. 아키텍처가 이미 여러분에게 주는 해법은 위의 패널 해부입니다. 트렌드는 버전 관리된 쿼리에 의해 매번 ts.sensor_reading으로부터 재생성되므로, 기록은 데이터와 질문이지 결코 이미지가 아닙니다.

알림 임계값은 한 가족 중 가장 거친 구성원이다

refId Coutside_range 테스트는 고정된 한계입니다. 36.5–37.5 °C, 모든 배치, 모든 캠페인에 대해 영원히 같은 밴드입니다. 그것은 규제 대상 제어 한계로서는 정확히 옳습니다 — CPP(핵심 공정 파라미터(critical process parameter), 즉 그 설정이 품질 결과에 영향을 미치는 공정 입력) 밴드는 검증되고 고정된 숫자이지, 모델이 다시 협상할 수 있는 것이 아닙니다. 그러나 온도 트렌드를 그리는 바로 그 패널은 분석 장이 구축하는 통계적 한계를 자연스럽게 짊어지는 곳이며, 그것들이 이 한계와 어떻게 다른지 정확히 짚어 둘 만합니다. SPC(통계적 공정 관리(statistical process control)) 차트는 손으로 설정한 밴드를, 데이터로부터 도출된 관리 한계로 대체합니다 — center ± 3·sigma, 여기서 산포는 이동 범위(moving-range) 방식으로 추정되므로 느린 드리프트가 정작 그것을 잡아내야 할 한계를 넓히지 못합니다. MSPC(다변량 SPC) 모니터는 단일 태그 테스트를, 여러 상관된 태그에 걸친 Hotelling's T² / SPE 쌍으로 대체합니다. 둘 다 여전히 구조적으로는 Grafana 패널입니다 — rawSql 쿼리(refId A)가 임계값(refId C)에 연결된 것. 그 전체 기계장치는 공정 분석: SPC, MVDA 및 소프트 센서에서 구축됩니다. 여기서의 요점은 다만, 대시보드의 임계값이 그 사다리의 가장 단순한 단(rung)이지 다른 무언가가 아니라는 것뿐입니다.

이것이 중요한 까닭은, 공정 을 지켜보는 바로 그 I-MR(개별값과 이동 범위(individuals and moving-range)) 관리도가 정확히 모델 을 지켜보는 도구이기 때문입니다. 소프트 센서의 예측 잔차(residual) — 예측 역가에서 느린 오프라인 분석값을 뺀 것 — 은 그 자체로 관리되어야 할 흐름이며, 중심에서 벗어나 드리프트하는 잔차는 모델이 낡아 가는 것, 즉 MLOps와 라이프사이클의 지연(lagging) "개념 드리프트(concept-drift)" 탐지기입니다. 그 발상에는 두 가지 주의가 따르며, 둘 다 이 대시보드에 내려앉습니다. 첫째, 소프트 센서 트렌드는 그것의 적용 영역(applicability domain) — 모델이 보정된 입력의 영역 — 안에서만 신뢰할 수 있습니다. Raman 보정은 그것의 정확한 프로브에 묶여 있으므로, 7일째 냉각 일탈(quality 플래그가 Uncertain으로 뒤집히는 때)은 밴드 안 데이터로 훈련된 모델이 외삽(extrapolation)하며 가장 신뢰할 수 없게 되는 바로 그 지점입니다 — 그래서 도출된 트렌드는 그 아래 원시 행 위에 자신만만한 선을 긋기보다 그 행들의 품질 플래그를 물려받아야 합니다. 둘째, 선행(leading)하는, 레이블이 필요 없는 동반 탐지기 — 입력 분포에 대한 인구 안정성 지수(Population Stability Index, PSI) — 는 새 로트나 오염되어 가는 프로브가 입력을 옮기는 순간, 오프라인 분석값이 어떤 오류를 확인하기도 전에 발화합니다. PSI를 잔차 차트 옆에 겹쳐 놓는 대시보드는 공정 드리프트와 모델 드리프트를 한 화면에서 읽습니다. 더 깊은 모델링과, 학습하는 모델을 GMP 변경 관리 아래 두는 검증 역설(validation paradox)은 ML & AI 책의 주제입니다. 대시보드 계층은 그 출력이 엔지니어가 실제로 지켜보는 무언가가 되는 곳입니다.

실제 현장에서는

맥락화된 실시간 공정 데이터 위의 운영자 및 엔지니어 대시보드는 현대적 mAb 라인에서 있으면 좋은 것이 아니라, 공정이 운전되고 제어되는 방식 그 자체이며, 점점 더 소프트 센서(soft sensor)와 다변량 분석(multivariate analytics)과 나란히 갑니다(공정 분석: SPC, MVDA 및 소프트 센서에서 처음부터 끝까지 구축되며, 더 깊은 모델링은 ML & AI 책에 있습니다) [1]. 대부분의 공장이 아는 상업용 기준점은 PI System 히스토리언 위에 자리한 AVEVA PI Vision입니다. 더 무거운 조사용 분석 — 골든 배치 오버레이, 다중 배치 비교, 근본 원인 트렌딩 — 에는 많은 공정 엔지니어가 Seeq를 집어 들며, 그 위의 리포팅 계층은 TIBCO SpotfireMicrosoft Power BI 같은 범용 비즈니스 인텔리전스(BI) 스위트가 맡습니다. Grafana는 우리가 여기서 구축하는 운영자·엔지니어 대시보드와 알림에 대한 신뢰할 만한 오픈소스 대응물입니다. Seeq의 배치 분석 깊이를 흉내 내려 하지 않으며, 실제 라인에서는 Grafana가 그런 상용 계층을 대체하기보다 그것과 나란히 돌아가는 경우가 많습니다. 파일럿 규모에서는 오픈 히스토리언 위의 오픈 대시보드 계층이 전적으로 합리적인 구축입니다.

이제 정직한 부분입니다. "노트북에서 멋지게 돌아간다"와 "GMP 출하 결정을 짊어진다" 사이를 가르는 두 가지 한계가 있습니다.

AGPLv3 조항. 2021년 Grafana Labs는 Grafana의 핵심(그리고 Loki와 Tempo)을 Apache 2.0에서 AGPLv3로 재라이선싱했습니다 [9]. AGPLv3는 Section 13, 즉 원격 네트워크 상호작용(remote network interaction) 조항을 더합니다. 여러분이 Grafana의 소스를 수정 한 뒤 그 수정된 버전을 네트워크를 통해 사용자에게 제공한다면, 그에 상응하는 수정된 소스를 그 원격 사용자들에게 제공해야 합니다 [10]. 압도적 대다수의 공장에 이것은 아무것도 바꾸지 않습니다. 수정하지 않은 공식 이미지를 자사 운영자들을 위해 내부적으로 돌리는 것은 어떤 소스 공개 의무도 부과하지 않습니다. 함정은 더 좁습니다. Grafana 자체를 포크(fork)하여 패치한 뒤 그 포크를 서비스로 노출하거나, 그것을 묶어 재배포하는 경우입니다. 이 책은 안전한 쪽에 머물기 위해 정확히 수정하지 않은 grafana-oss 이미지를 제공하고, AGPLv3를 라이선스 인벤토리에 표시하여 그 의무가 감사 중의 깜짝 사건이 아니라 문서화된 결정이 되도록 합니다.

검증의 마지막 한 마장(last mile). Grafana는 상자에서 꺼내자마자 21 CFR Part 11 / Annex 11을 준수하지 않습니다 — 어떤 OSS 도구도 그렇지 않으며, 이는 스택 전체에 해당합니다. 준수는 다운로드가 아니라 검증된 시스템 더하기 절차(validated system plus procedures) 의 속성입니다. Grafana OSS는 코드로 만드는 대시보드, 프로비저닝된 신원, 그리고 데이터로부터 재현 가능한 쿼리를 줍니다 — 여기서 순수 오픈 소스가 진정으로 전달할 수 있는 그 약 80%입니다. GxP 마지막 한 마장에서 하이브리드 현실이 물어뜯습니다. Grafana OSS에는 네이티브 Part 11 전자 서명(e-signature)이 없고, 그것의 더 풍부한 접근 제어(세분화된 RBAC — 역할 기반 접근 제어(role-based access control) — 더하기 SSO/SAML을 통한 단일 로그온(single-sign-on), 리포팅, 데이터 소스 권한)는 OSS 빌드가 아니라 Grafana Enterprise / Cloud에 있습니다. 규제 대상 배포는 Grafana 주위에 둘러친 검증된 시스템으로 그 간극을 메웁니다 — 신원 공급자(identity provider), 데이터베이스 안의 감사 추적, 프로비저닝 저장소에 대한 변경 관리(change control), 그리고 출하 트렌드는 저장된 이미지로 신뢰하는 일이 결코 없이 히스토리언으로부터 재생성한다고 명시하는 표준 운영 절차(standard operating procedure, SOP)입니다. 대시보드는 오픈 소스이고, 신뢰는 여러분이 그 주위에 세우는 시스템에서 옵니다.

핵심 용어

  • 코드로 만드는 대시보드(dashboard-as-code) — Grafana 데이터 소스, 대시보드, 알림을 UI에서 클릭하는 대신 시작 시 로드되는 버전 관리된 YAML/JSON 파일로 정의하는 것.
  • 프로비저닝(provisioning) — 부팅 시 마운트된 디렉터리에서 그 파일들을 읽는 Grafana 메커니즘. 여기서는 UI가 진실의 기록을 덮어쓸 수 없도록 읽기 전용으로 마운트됨.
  • 데이터 소스(data source) — Grafana가 필요할 때마다 쿼리하는 구성된 연결(여기서는 uid: timescaledb). Grafana는 데이터 자체를 전혀 저장하지 않음.
  • 패널 / 타깃(panel / target) — 단일 시각화와 그것을 먹이는 SQL 쿼리. 트렌드가 저장된 그림이 아니라 재현 가능한 질문 인 이유.
  • 템플릿 변수(templating variable, $batch) — 그 자체가 쿼리로 정의되는 대시보드 변수. 드롭다운으로 노출되어 모든 패널의 SQL에 치환되므로, 하나의 대시보드가 어떤 배치든 섬길 수 있음.
  • 임계값 표현식(threshold expression, refId C) — 알림 규칙의 두 번째 단계. evaluator(여기서는 outside_range, params [36.5, 37.5])가 쿼리 노드(refId A)가 돌려준 행들을 테스트하여 발화 여부를 결정하는 threshold 노드.
  • 연락 지점 / 알림 정책(contact point / notification policy) — 규칙이 발화할 때 Grafana가 어디로 어떻게 알림을 보내는지.
  • AGPLv3 Section 13수정된 Grafana를 네트워크를 통해 제공할 때 소스 공개를 요구하는 네트워크/원격 상호작용 조항.
  • PI Vision — AVEVA의 상업용 공정 시각화 제품. Grafana는 이 장이 구축하는 그 오픈소스 대응물.
  • 데이터로부터 재현 가능(reproducible-from-data) — 증거로 쓰이는 트렌드가 검증된 기록으로부터 재생성 가능해야 한다는 규제 규칙. 스크린샷은 그러지 못함.
  • 적용 영역(applicability domain) — 모델이 보정된 입력의 영역. 소프트 센서 트렌드는 그 안에서만 신뢰할 수 있으므로, quality 플래그를 뒤집는 일탈은 도출된 선이 가장 신뢰할 수 없는 지점이기도 함.
  • 드리프트 탐지기(drift detector, PSI / 잔차 차트) — 선행하는, 레이블이 필요 없는 입력 분포 모니터(인구 안정성 지수)와 지연하는 잔차 관리도. 함께 읽으면 같은 대시보드에서 모델 드리프트와 공정 드리프트를 가름.
  • SHACL 게이트outside_range 알림의 시맨틱 쌍둥이인 닫힌 세계 그래프 제약(sh:minInclusive/sh:maxInclusive). 살아 있는 신호의 거동이 아니라 기록의 완전성과 적합성을 지킴.

같은 트렌드를, 시맨틱하게 말하면

이 페이지는 안정적인 식별자에 크게 기댑니다 — timescaledb 데이터 소스 uid, BR101.Temp.PV 태그, BATCH-2026-001 배치 id — 그러면서도 그것들을 다음 장이 부를 이름, 즉 그래프의 출발점이라고는 한 번도 부르지 않습니다. 그 고리를 닫아 둘 만한 것은, 패널과 알림이 각각 정확한 시맨틱 쌍둥이를 갖기 때문입니다. 태그 문자열 BR101.Temp.PV는 평평한 키입니다. 같은 사실을 RDF 트리플(triple)(주어–술어–목적어, 지식 그래프의 원자)로 진술하면 bp:BR101 bp:hasMeasurement bp:BR101.Temp.PV가 되고, 하나의 판독값은 bp:reading-1 bp:value 37.01 ; bp:hasQuality bp:Good ; bp:ofBatch bp:BATCH-2026-001이 됩니다 — 여기서 bp:Good은 이제 그래프가 추론하는 유형이 부여된 사물 이지, 그 의미가 DDL 주석 안에만 사는 정수 192가 아닙니다. "열 안의 192"에서 이름이 부여된 품질 클래스로의 그 이동이 값과 어휘(vocabulary)를 가르는 차이이며, 그것이 바로 온톨로지 책이 하는 식별자와 단위클래스와 분류체계 작업입니다. 히스토리언에서 그래프로의 적재 자체는 이 책의 시맨틱과 디지털 스레드 장이 실행 가능하게 다루는 주제입니다.

이 페이지의 메커니즘 둘이 특히 깔끔하게 대응합니다. 운영자의 $batch 드롭다운 — 히스토리언이 아는 배치를 나열하라 — 은 컴피턴시 질문(competency question)(데이터 모델이 답할 수 있어야 하는 질문)이고, 그것의 SQL SELECT DISTINCT batch_id는 SPARQL SELECT ?batch WHERE { ?r bp:ofBatch ?batch }의 관계형 형제입니다. 그리고 알림 규칙은 거의 문자 그대로 하나의 제약(constraint)입니다 — "출하된 모든 로트의 온도 기록은 그 밴드 안에 있어야 한다"는 닫힌 세계(closed-world)의 게이트 이며, 그것이 SHACL(Shapes Constraint Language — 그래프 데이터가 요구된 구조를 갖추었는지 검증하는데, 누락되거나 밴드를 벗어난 결과는 열린 질문이 아니라 지금 당장의 실패입니다)의 일입니다. sh:minInclusive 36.5 ; sh:maxInclusive 37.5를 갖는 SHACL sh:propertyoutside_range params: [36.5, 37.5]가 Grafana에서 말하는 것을 그래프에서 정확히 말합니다 — 차이라면 SHACL은 계보(genealogy) 이음매에서 기록의 완전성과 적합성 을 지키고, Grafana 규칙은 살아 있는 신호의 거동 을 지킨다는 것입니다. 그 발상의 출하 게이트 버전, 즉 진행 중인 캠페인에 대해 검증되는 것은 출하 게이트와 SHACL이며, 하나의 쿼리가 배치 → 풀 → 원료의약품을 걷게 하는 계보 엣지(derivedFrom)는 관계와 계보입니다. 대시보드는 그림이고, 그래프는 그 위 모든 레이블 뒤의 의미입니다.

다음 이야기

우리는 이제 데이터를 볼 수 있고 그 그림을 신뢰할 수 있습니다. 하지만 Grafana, 히스토리언, 배치 모델은 여전히 "같은 것"을 조금씩 다른 방언으로 말합니다. 다음 장 시맨틱과 디지털 스레드: 온톨로지와 지식 그래프(Semantics & the Digital Thread: Ontologies and a Knowledge Graph) 는 공장 전체에 하나의 공유된 의미를 부여합니다 — 장비, 물질, 레시피, 결과를 온톨로지(ontology)로 모델링하고 이를 RDF/SPARQL 지식 그래프(knowledge graph)로 엮어, 단일한 핵심 품질 속성(critical quality attribute)을 업스트림, 다운스트림, QC에 걸쳐 하나의 쿼리로 추적할 수 있게 합니다.