REST API (v1)
PlantPulse Edge 의 v1 REST API 매뉴얼. 화면 / 외부 시스템이 OPC, 태그, 모니터링, 외부 전송 헬스체크를 다루는 표준 인터페이스 입니다.
| 항목 | 값 |
|---|---|
| Base URL | http(s)://<edge-host>/api/v1 |
| Content-Type | application/json; charset=utf-8 |
| 인증 | API Key 강제 (edge.rest.api.auth=true) — X-API-Key / Authorization: Bearer, 또는 로그인 세션으로 통과. ?api_key= 는 401 로 거부 |
| Audit 로그 | ApiAccessLogFilter — 모든 /api/* 호출을 한 줄로 기록 (메서드/경로/상태/지속/IP/사용자/auth 모드) |
| 메인 컨트롤러 | plantpulse.app.edge.api.v1.* (OpcAPI, TagAPI, AppAPI, SystemAPI, MonitoringAPI) |
| 오류 envelope | {data, meta, errors} — filter/advice 경로도 동일 |
| HTTPS | edge.rest.api.https_only=true 기본. plain HTTP client 호출은 거부 또는 Tomcat HTTPS redirect 정책을 따른다 |
인증 (ApiAuthFilter)
app.properties 에서 edge.rest.api.auth=true 로 활성화. 키는 edge.rest.api.key 이며 운영에서는 EDGE_REST_API_KEY_FILE 또는 EDGE_REST_API_KEY 로 주입합니다.
다음 3가지 중 하나라도 만족하면 통과:
A. 로그인 세션 통과 (브라우저)
이미 /login/form 으로 로그인한 사용자 — 세션에 _USER_LOGIN attribute (JSONObject) 가 있으면 ApiAuthFilter 가 그대로 chain.doFilter() 합니다. 화면(JSP) 에서 ajax 로 호출하는 모든 /api/* 가 추가 키 없이 동작합니다.
B. X-API-Key 헤더 (권장)
EDGE_REST_API_KEY="$(tr -d '\r\n' < /run/secrets/edge-rest-api-key)"
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf 'header = "X-API-Key: %s"\n' "$EDGE_REST_API_KEY" > "$cfg"
curl --config "$cfg" https://edge.example.com/api/v1/tag/TAG_LS_XBC_0001/value
C. Authorization: Bearer <키> 헤더
EDGE_REST_API_KEY="$(tr -d '\r\n' < /run/secrets/edge-rest-api-key)"
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$EDGE_REST_API_KEY" > "$cfg"
curl --config "$cfg" https://edge.example.com/api/v1/tag/TAG_LS_XBC_0001/value
D. ?api_key=<키> 쿼리 파라미터 — 거부됨
URL 에 키가 평문으로 들어가 액세스 로그/프록시 캐시/브라우저 history 에 남을 수 있으므로 v1 에서는 401 로 거부합니다. 헤더 방식(B/C)을 사용하세요.
실패 응답
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
{
"data": null,
"meta": {
"timestamp": 1778069486934,
"status": "ERROR",
"http_status": 401,
"request_id": "3b89b8f1-0fc1-4ab4-b4eb-d12dc314d500"
},
"errors": [
{
"code": "UNAUTHORIZED",
"message": "Invalid or missing X-API-Key."
}
]
}
키 비교는 constantTimeEquals() (timing-attack 방어).
edge.rest.api.auth=false (개발 / 임시)
인증을 끄면 ApiAuthFilter 가 즉시 chain.doFilter() — 외부에서 키 없이 호출 가능합니다. 운영망에서는 항상 true 권장.
Audit 로그 (ApiAccessLogFilter)
모든 /api/* 호출을 한 줄 로그로 기록 — 인증 실패(401) 도 포함되어 audit 트레일을 남깁니다 (필터 체인 순서: ApiAccessLogFilter → ApiAuthFilter).
형식
[API_ACCESS] method=GET path=/api/v1/tag/TAG_MS_1000/value query= status=200 duration=12ms
ip=100.127.147.98 user=admin auth=session ua=Mozilla/5.0...
| 필드 | 의미 |
|---|---|
method | HTTP 메서드 |
path | request URI (쿼리 미포함) |
query | 쿼리 문자열 — 거부되는 ?api_key=... 부분도 *** 로 마스킹 |
status | HTTP 응답 코드 |
duration | 처리 시간 (ms) |
ip | 기본은 RemoteAddr. edge.rest.api.trust_x_forwarded_for=true 인 reverse proxy 배치에서만 X-Forwarded-For 첫 IP |
user | session 사용자면 user_id / apikey 면 XXXX*** (len=16) (앞 4자 + 길이) / 익명이면 - |
auth | session (브라우저 로그인) / apikey (X-API-Key/Bearer) / deprecated_query (거부된 ?api_key= 시도) / none (키 누락 또는 익명) |
ua | User-Agent (80자 초과 시 … 잘라짐) |
error | 예외 발생 시 error=ClassName 추가 |
로그 레벨 분기
| 상태 | 레벨 | 의도 |
|---|---|---|
| 5xx 또는 throwable | ERROR | 서버 오류 — 즉시 알람 |
| 401, 403 | WARN | 인증/권한 거절 — audit 우선 |
| 그 외 (2xx, 3xx, 4xx 의 4xx 일반) | INFO | 일상 호출 |
활용
- 누가 키를 잘못 보내고 있나 —
grep '\[API_ACCESS\].*status=401'로 401 트레일 확인 - 5xx 회귀 추적 —
grep '\[API_ACCESS\].*status=5'+error=필드 - 호출 빈도 / 사용자별 집계 — auth=apikey/session 로 grouping, ip 별 분석
- 응답 지연 슬로우 쿼리 —
duration=큰 라인 추출
키 평문은 어디에서도 로그에 남지 않습니다 — query 마스킹 + user 필드 prefix 4자 만으로 식별. 침해 사고 분석 에는 충분한 식별성 + 평문 노출 방지의 균형.
web.xml 매핑 순서: ApiAccessLogFilter 가 먼저, 그 다음 ApiAuthFilter. AccessLog 가 finally 블록에서 status 를 기록하기 때문에 인증 실패한 401 호출도 그대로 audit 됩니다. 순서가 바뀌면 401 호출이 access log 에서 누락됩니다.
응답 envelope
v1 REST API 응답은 공통 envelope 으로 감쌉니다 (RestApiSupport, ApiEnvelopeWriter).
정상 응답:
{
"data": { },
"meta": {
"timestamp": 1778069486934,
"status": "OK",
"request_id": "f818abd0-eb0f-4497-9b07-7ccc94b2e2d7"
}
}
에러 응답:
{
"data": null,
"meta": {
"timestamp": 1778069486934,
"status": "ERROR",
"http_status": 400,
"request_id": "3b89b8f1-0fc1-4ab4-b4eb-d12dc314d500"
},
"errors": [
{
"code": "VALIDATION_FAILED",
"message": "Validation failed"
}
]
}
옛 RPC-style envelope (result, _session_id, _user_id) 는 REST v1 에서 사용하지 않습니다.
1. 엔드포인트 일람
1.1 OPC
| 슬러그 | Method | URL | 설명 |
|---|---|---|---|
flow_edge_opc_list | GET | /api/v1/opc | OPC 목록 (tag_count / connection_status / scan_status 포함) |
flow_edge_opc_create | POST | /api/v1/opc | OPC + 태그 묶음 등록 |
flow_edge_opc_update | PUT | /api/v1/opc/{opcId} | OPC + 태그 묶음 갱신 (path opcId 가 body opc_id 를 덮어씀) |
flow_edge_opc_delete | DELETE | /api/v1/opc/{opcId} | OPC 및 하위 태그 일괄 삭제 |
flow_edge_opc_start | POST | /api/v1/opc/{opcId}/start | 수집 시작 |
flow_edge_opc_stop | POST | /api/v1/opc/{opcId}/stop | 수집 중지 |
1.2 Tag
| 슬러그 | Method | URL | 설명 |
|---|---|---|---|
flow_edge_tag_list | GET | /api/v1/opc/{opcId}/tag | 특정 OPC 의 태그 목록 (마지막 값/상태 포함) |
flow_edge_tag_create | POST | /api/v1/opc/{opcId}/tag | 단건 태그 등록 |
flow_edge_tag_update | PUT | /api/v1/tag/{tagId} | 단건 태그 부분 갱신 (기존 row 머지 후 upsert) |
flow_edge_tag_delete | DELETE | /api/v1/tag/{tagId} | 단건 태그 삭제 |
flow_edge_tag_read | GET | /api/v1/tag/{tagId}/value[?fresh=true] | 캐시 값 (default), fresh=true 면 PLC 직접 read |
flow_edge_tag_write | POST | /api/v1/tag/{tagId}/value | 값 쓰기 — 모든 프로토콜 지원 (HTTP / OPC-UA / Modbus / MELSEC / S7 / LS / EIP) |
1.3 Monitoring / Edge / Transfer
| 슬러그 | Method | URL | 설명 |
|---|---|---|---|
flow_edge_monitoring | GET | /api/v1/monitoring | CPU/메모리/네트워크/디스크/스레드 등 MonitorBean 의 모든 필드 |
flow_edge_info | GET | /api/v1/edge | 엣지 식별/버전/사이트/OS/가동시간 메타 |
flow_edge_transfer | GET | /api/v1/transfer | outbound transfer (API/MQTT/Sparkplug) 별 헬스 상태 |
1.3a System (인증 불필요 — LB / k8s probe / OTA 용)
/api/v1/system/* 중 아래 3개는 ApiAuthFilter 화이트리스트 통과 (어떤 OPC/Tag 정보도 노출 안 함).
| Method | URL | 설명 | HTTP |
|---|---|---|---|
| GET | /api/v1/system/health | 8개 컴포넌트 (cassandra / redis / mqtt / node_red / opc_ua / edge_core / tse / grafana) TCP 200ms probe + 종합 status | 200 (all UP) / 503 (DEGRADED) |
| GET | /api/v1/system/ready | monitor + V5 api_client 준비 상태 | 200 / 503 |
| GET | /api/v1/system/version | Metadata.VERSION + BUILD_DATE + /etc/kopens/version.env image_tag + /.dockerenv container_mode | 200 |
/health 예시:
{
"data": {
"status": "UP",
"uptime_ms": 1720008,
"components": {
"cassandra": "UP", "redis": "UP", "mqtt": "UP",
"node_red": "UP", "opc_ua": "UP"
}
}
}
/version 예시:
{
"data": {
"product_name": "PlantPulse Edge",
"version": "2026",
"build_date": "20260523",
"image_tag": "2026-20260523",
"container_mode": true
},
"meta": {
"timestamp": 1778925572946,
"request_id": "..."
}
}
OTA upgrade.sh 가 /api/health (300초) probe 로 auto-rollback 판단. Docker HEALTHCHECK 도 /health 사용.
1.4 OPC-UA 뷰어 (OPCUAViewerAPI)
내장 OPC-UA 서버의 endpoint / 노드 트리 / 시계열 — /ui/opcua 화면이 사용.
| Method | URL | 설명 |
|---|---|---|
| GET | /ui/opcua/info[?reveal=true] | 내장 OPC-UA 서버의 endpoint URL (TCP/TLS) / Application 이름 / 인증 정보 (reveal=true + 인증 세션 시 password 평문) |
| GET | /ui/opcua/tree | Site → OPC → Tag 트리 (NodeId 포함). EDGE_* 시스템 OPC 는 항상 connection_status=CONNECTED |
| GET | /ui/opcua/history?tagId=...&minutes=N&limit=M | 카산드라 tm_tag_point 의 시계열 조회. 입력 검증 후 ASC 정렬된 points 배열 반환 |
1.5 업그레이드 / 시스템 (ConfigAPI)
/config/* 는 v1 REST API 와 별도 (관리자 화면용) 이지만 본 매뉴얼에서 같이 정리.
| Method | URL | 설명 |
|---|---|---|
| GET | /config/upgrade/check | server-to-server 로 product.kopens.io 의 VERSION.JSON 가져와 {result, latest_version, latest_build_date} 반환. 실패 시 result=ERROR |
| POST | /config/upgrade | bin/upgrade.sh 실행 (장시간 작업) |
| POST | /config/restart | bin/restart.sh 실행 (Tomcat 재시작) |
| POST | /config/reboot | bin/reboot.sh 실행 (OS 재부팅) |
| POST | /config/temp-clean | bin/clean.sh 실행 (로그/임시파일 정리) |
| POST | /config/backup | bin/backup.sh 실행 |
| POST | /config/firmware | bin/firmware.sh 실행 (dnf update -y) |
| GET | /config/load | app.properties 텍스트 반환 |
| POST | /config/save | app.properties 저장 |
2. 요청 / 응답 예시
2.1 GET /api/v1/opc — OPC 목록
curl -s http://<edge-host>/api/v1/opc | jq
{
"result": "OK",
"data": {
"data": [
{
"opc_id": "OPC_UA_Kepware",
"opc_type": "OPCUA",
"opc_name": "OPC_UA_Kepware",
"opc_agent_ip": "192.168.0.40",
"opc_agent_port": "49320",
"auto_collect": "true",
"timecycle": "1000",
"options": {"username": "kopens", "password": "***", "discovery": "false"},
"tag_count": 11,
"point_count": 5819,
"connection_status": "CONNECTED",
"scan_status": "START"
}
]
}
}
⚠
data.data[]형태 (이중 wrap) — 위 envelope 섹션 참고.
2.2 POST /api/v1/opc — OPC + 태그 묶음 등록
curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_NEW",
"opc_type": "OPCUA",
"opc_name": "신규 연결",
"opc_agent_ip": "10.0.0.10",
"opc_agent_port": "49320",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"tag_list": [
{"tag_id": "TAG_001", "tag_name": "Sine",
"plc_address": "ns=2;s=Sine1", "data_type": "Float"}
]
}'
2.3 POST /api/v1/opc/{opcId}/tag — 태그 단건 등록
{
"tag_id": "TAG_NEW",
"tag_name": "신규 태그",
"plc_address": "ns=2;s=NewTag",
"data_type": "Float",
"description": "..."
}
응답:
{ "result": "OK", "data": { "result": "SUCCESS", "tag_id": "TAG_NEW" } }
site_id 는 OPC 에서 자동으로 채워 넣습니다.
2.4 PUT /api/v1/tag/{tagId} — 태그 부분 갱신
요청 body 는 변경할 필드만 보냅니다. 서버에서 기존 row 와 머지 후 upsert.
{ "description": "변경된 설명" }
2.5 GET /api/v1/tag/{tagId}/value — 마지막 값 조회
기본 (캐시):
curl -s http://<edge-host>/api/v1/tag/TAG_UA_0004/value | jq
{
"result": "OK",
"data": {
"tag_id": "TAG_UA_0004",
"value": "28.9688",
"value_time": "2026-05-06 20:59:39.000",
"value_read_status": "SUCCESS",
"value_read_error_message": ""
}
}
fresh=true (PLC 직접 read):
curl -s 'http://<edge-host>/api/v1/tag/TAG_UA_0004/value?fresh=true' | jq
PLC read 실패 시 외부 result=ERROR + message:
{ "result": "ERROR", "message": "fresh read failed: ..." }
⚠ 존재하지 않는 태그를 조회해도 envelope
result=OK로 응답하며value가"-"로 채워진 placeholder 페이로드를 돌려줍니다 (현재 동작).
2.6 POST /api/v1/tag/{tagId}/value — 값 쓰기
curl -X POST http://<edge-host>/api/v1/tag/TAG_HTTP_001/value \
-H "Content-Type: application/json" \
-d '{ "value": "42" }'
성공:
{
"result": "OK",
"data": {
"result": "SUCCESS",
"opc_id": "OPC_LS_TEST",
"plc_address": "D1000",
"value": "42",
"actual_read": "42"
}
}
실패 (드라이버가 false 반환 / OPC DISCONNECTED / 스케줄러 미가동) — 외부 envelope result=ERROR:
{ "result": "ERROR", "message": "쓰기 실패 (opc_type=EIP) — EIP 펌웨어/패치에 따라 ..." }
지원 프로토콜: HTTP / OPC-UA / Modbus / MELSEC / S7 / LS XGT / EIP — 모두 가능. EIP 는 PLC 펌웨어/패치별 제약 가능.
2.7 POST /api/v1/opc/{opcId}/start — 수집 시작
curl -X POST http://<edge-host>/api/v1/opc/OPC_NEW/start
내장 OPCUA/MODBUS 시뮬레이터 자동 기동은 지원하지 않습니다. 테스트/데모 데이터는 외부 plantpulse-simulator 또는 테스트 태그를 사용하세요.
2.8 GET /api/v1/edge — 엣지 식별 / 가동 정보
EdgeAPI.edgeInfo() 가 직접 채우는 필드:
{
"result": "OK",
"data": {
"id": "EDGE_00303",
"site_id": "SITE_00001",
"site_name": "S1_LOTTE_CS_DJ_SITE",
"hostname": "EDGE-303",
"product_name": "PlantPulse Edge",
"version": "2026",
"build_date": "2026-05-08",
"os_name": "Linux",
"os_version": "6.14.5-100.fc40.x86_64",
"os_arch": "amd64",
"started": true,
"started_date": 1778066700000,
"uptime_ms": 124500,
"always_on": false,
"ttl": 30,
"sended_count": 8063
}
}
2.9 GET /api/v1/monitoring — 시스템 메트릭
MonitorBean 의 모든 필드를 그대로 직렬화. 1초 갱신.
curl -s http://<edge-host>/api/v1/monitoring | jq '.data | keys'
대표 필드: cpu_used_percent, memory_used_percent, disk_used_percent, temperature, ping, api, opc_count, tag_count, mps, mps_history, queue_size, plc_value_read_success_count, plc_value_read_error_count, plc_value_write_success_count, plc_value_write_error_count, plc_con_connected_count, plc_con_disconnected_count, plc_scan_start_count, plc_scan_not_collect_count, plc_scan_stop_count, sended_point_count, sended_point_bytes, system_total_db_size, system_error_count, docker_on, docker_container_up_count, docker_container_total_count.
2.10 GET /api/v1/transfer — 외부 전송 헬스
curl -s http://<edge-host>/api/v1/transfer | jq
{
"result": "OK",
"data": {
"transfers": [
{ "type": "API", "enabled": true, "connected": true, "sent_count": 8063 },
{ "type": "MQTT", "enabled": true, "connected": true, "sent_count": 12345 },
{ "type": "Sparkplug", "enabled": true, "connected": true,
"group_id": "Plant1", "edge_node_id": "EDGE_00303",
"bdSeq": 7, "seq": 211 }
]
}
}
각 transfer 의 getStatus() 결과를 그대로 배열로 반환. 호출 중 예외 발생 시 {type, status_error} 로 폴백.
2.11 GET /ui/opcua/info — 내장 OPC-UA 서버 정보
curl -s http://<edge-host>/ui/opcua/info | jq
{
"result": "OK",
"data": {
"application_name": "PlantPulse Edge OPC-UA Server",
"product_uri": "urn:plantpulse:opcua:server",
"domain": "127.0.0.1",
"tcp_endpoint": "opc.tcp://127.0.0.1:12000",
"tls_endpoint": "opc.tcp://127.0.0.1:12443",
"namespace_index": 2,
"auth_username": "edge",
"auth_password_set": true,
"auth_anonymous": false
}
}
?reveal=true + 인증된 세션이면 auth_password 평문 추가.
2.12 GET /ui/opcua/tree — 노드 트리
Site → OPC → Tag 3단 트리. 각 태그에 NodeId, 최신 값, 데이터 타입, description 포함. EDGE 시스템 OPC 는 connection_status=CONNECTED 강제.
NodeId 컨벤션: ns=2;s=<SITE>.<OPC>.<TAG>.
2.13 GET /ui/opcua/history — 시계열
| 파라미터 | 기본 | 제한 |
|---|---|---|
tagId | 필수 | 최대 200 자 |
minutes | 10 | 1 – 1440 (24h) |
limit | 600 | 1 – 5000 |
curl -s 'http://<edge-host>/ui/opcua/history?tagId=TAG_S7_1000&minutes=10&limit=100' | jq
{
"result": "OK",
"data": {
"tag_id": "TAG_S7_1000",
"minutes": 10,
"count": 100,
"points": [
{ "ts": 1778176880010, "value": "211", "type": "integer", "quality": 192 }
]
}
}
tagId 누락/공백/200자 초과 시 외부 result=ERROR (message). 카산드라 timestamp 가 wrap 객체로 와도 extractTimestampMs 로 epoch ms 변환.
3. 에러 응답 패턴
| 케이스 | 응답 형태 |
|---|---|
| 외부 envelope ERROR (write 실패 / fresh read 실패 / opcua/history 입력 검증 실패 / upgrade/check 다운로드 실패) | {result:"ERROR", message:"..."} (HTTP 200) |
| 내부 비즈니스 ERROR (CRUD 시 tag/opc not found / DB 무결성 / 스케줄러 미가동) | {result:"OK", data:{result:"ERROR", msg:"..."}} (HTTP 200) |
| Spring 미스매핑 (필수 파라미터 누락, 잘못된 메서드 등) | HTTP 500 + 스택트레이스 — 권장: 컨트롤러 단에서 @RequestParam(required=false) 처리 후 envelope 으로 변환 |
인증 — edge.rest.api.auth 가 설치 기본값 true 이며 ApiAuthFilter 가 X-API-Key / Bearer 를 요구한다(위 인증 절 참조). 헬스 엔드포인트만 예외 |
4. 동작 메모
- 태그 단건 조회:
APP_TAG.xml의R03(WHERE TAG_ID = :tag_id ALLOW FILTERING). - OPC + 태그 묶음 등록 / 단건 태그 등록 후
ConnectService.restartComponents()호출 →OPCAndTagsCachereload 및 OPC-UA 서버 재시작. - 값 쓰기 (
tagWriteByTagId): HTTP 는HTTPDriver.bind(), 그 외는driver.write(ProtocolAddress). 실패 시 (ok=false) 친절 힌트 (EIP 펌웨어 호환성 등) 포함. write 후 즉시 read 한 결과를actual_read로 함께 반환 (HTTP 제외). - 트랜스퍼 헬스 (
/api/v1/transfer):TransferRegistry에 등록된 모든 outbound 컴포넌트의getStatus()를 모음. Sparkplug B 카드 (/ui/main) 가 5초 주기로 호출. - OPC-UA 뷰어 (
/ui/opcua/*) 는 내부망 폴링 (2초) —OPCAndTagsCache.getInstance()+LastValueMap.getInstance()만 사용해 부하 적음. - 레거시 deprecated:
/api/http(APIController) —/api/v1/tag/{tagId}/value사용 권장.
5. 코드 위치
- 컨트롤러:
src/plantpulse/app/edge/api/v1/OpcAPI.java · api/v1/TagAPI.java - OPC-UA 뷰어:
src/plantpulse/app/edge/module/opcua/OPCUAViewerController.java - 업그레이드 프록시:
src/plantpulse/app/edge/module/system/config/ConfigController.java - 비즈니스 로직:
src/plantpulse/app/edge/module/connect/ConnectService.java - 응답 envelope helper:
plantpulse.app.edge.core.web.ControllerSupport(ok()/error())