Skip to content

Edge Observability Transport Model (current target version)

This document is the latest authoritative description of "how Agent ↔ Server observability data is transmitted".
After reading you should be able to answer: what is sent, where it is collected from, how often, how the Server stores it, and where product metrics are queried from.
Protocol fields and DDL details: Observability Reporting Protocol & Data Model; background: Edge Observability & Business Traffic Stats.


0. Remember the Three Layers First

LayerQuestion AnsweredSingle Data SourceProduct Examples
L1 Business deliveryHow much data provided? How many requests?access.log detailsdata provided, request count, UV, status codes, Top domains
L2 Edge healthIs OpenResty alive? Current connections?local /openflare/observabilitynode health, current connections
L3 Host capacityHow are CPU/memory/disk/NIC?OS readingscapacity trends, host NIC

The three layers are never reconciled against each other.
"Data provided" ≠ "current connections" ≠ "host NIC outbound".


1. Overview: Who Collects, Who Reports, Who Aggregates

text
┌─────────────────────────────────────────────────────────────┐
│ Edge Node                                                    │
│                                                               │
│  Visitor request ──► OpenResty                                 │
│                 │                                              │
│                 ├─ access.log (one line per request)  ←── L1 collection point │
│                 │                                              │
│                 └─ connection state (maintained in-process)    │
│                        │                                       │
│                        ▼                                       │
│              GET /openflare/observability  ←── L2 reads snapshot │
│              (no log scanning, no business recomputation)      │
│                                                               │
│  OS /proc etc.  ──────────────────────────── L3 reads snapshot │
│                                                               │
│              ┌────────── Agent ──────────┐                     │
│              │ default: one NodePayload per 3s  │              │
│              │  · tail access.log incremental │               │
│              │  · GET local observability     │               │
│              │  · read host_metrics           │               │
│              └────────────┬──────────────┘                     │
└─────────────────────────────│──────────────────────────────────┘
                              │ HTTP heartbeat or WebSocket status

┌─────────────────────────────────────────────────────────────┐
│ Server (control plane)                                       │
│  · details → ClickHouse of_node_access_logs                  │
│  · health → node latest state + of_node_edge_health          │
│  · host → of_node_metric_snapshots                           │
│  · business trends / Zone stats = sum/count/uniq over access_logs only │
└─────────────────────────────────────────────────────────────┘
RoleDoesDoesn't
OpenRestywrites access.log; maintains connection countsno direct reporting to the control plane
Agentcollects facts and reports themdoes not compute UV/TopN/24h data provided
Serverstores + aggregates/interpretersdoes not trust edge business pre-summaries

2. Collection Frequency (defaults)

ActionDefault FrequencyConfig
Agent → Server reportevery 3 seconds a full payloadheartbeat_interval / control-plane agent_heartbeat_interval (ms, default 3000)
Tail access.log when packingwith report (new lines since last report)same
GET /openflare/observability when packingwith report (reads current connection snapshot)same
Read host metrics when packingwith reportsame
OpenResty writes access.log1 line at each request endunrelated to heartbeat
Connection counts update in-processon connection change (kernel-maintained)unrelated to heartbeat
Offline replay windowkeep ~60 minutes by defaultobservability_replay_minutes
Node offline detection~60s without a successful heartbeatnode_offline_threshold (default 60000 ms)

Notes:

  • The Agent has no separate "sampling clock"; sampling points = report points (default 3s).
  • access.log is "per-request continuous writes"; the Agent only moves incremental lines periodically.
  • /openflare/observability is not "business stats start being counted when called"; for connections it reads Nginx's existing instantaneous values.

Transport channels:

  • HTTP heartbeat: POST the full payload at the interval.
  • WebSocket: after connecting, sends status messages at the same interval (same content shape); HTTP heartbeat is not double-sent then.

3. Agent → Server Packet (NodePayload v2)

3.1 Structure Skeleton

json
{
  "schema_version": 2,
  "node_id": "n_01hxyz",
  "name": "edge-shanghai-1",
  "ip": "203.0.113.10",
  "version": "3.4.0",
  "ext_version": "",
  "current_version": "20260718-abc",
  "last_error": "",
  "profile": { },
  "host_metrics": { },
  "edge_health": { },
  "access_logs": [ ],
  "buffered": [ ],
  "health_events": [ ],
  "waf_ip_group_checksums": { }
}
FieldLayerMeaning
identity/version/last_errorcontrolwho the node is, what version it runs
profilelow-frequency overviewhostname, core count, etc. (report on change)
access_logsL1access detail increments
edge_healthL2OpenResty health + current connections
host_metricsL3CPU/memory/disk/NIC readings
bufferedbackfillbatches of facts accumulated while offline
health_eventseventse.g. openresty_unhealthy
waf_ip_group_checksumssyncnot an observability lake

Removed from the protocol (no compatibility layer; old Agents must upgrade):

  • traffic_report
  • openresty_observation (incl. rx/tx)
  • snapshot / buffered_observability
  • business-meaning openresty throughput fields

4. L1 Business: access_logs

4.1 Where Collection Comes From

StepLocationDescription
1OpenResty log_format openflare_jsonwrites one JSON line per request to access_log_path
2Agent tails increments by file offsetnew lines between two heartbeats
3parse and put into access_logs[]overlong paths may be truncated; no sum/count

Log format (OpenResty variables):

text
ts            ← $time_iso8601
host          ← $host
path          ← $request_uri
remote_addr   ← $remote_addr
status        ← $status
request_time  ← $request_time
bytes_sent    ← $body_bytes_sent     【data provided = response body bytes】
request_length← $request_length      【data received】
user_agent    ← $http_user_agent
cache_status  ← $upstream_cache_status  【cache status; UI can derive hit/origin/un-cached】

Observability-port requests don't write business access.log (separate server with access_log off).

4.2 Report Example

json
"access_logs": [
  {
    "logged_at_unix": 1721289601,
    "remote_addr": "198.51.100.20",
    "host": "www.example.com",
    "path": "/api/v1/ping",
    "status_code": 200,
    "bytes_sent": 1024,
    "request_length": 128,
    "request_time_ms": 15,
    "user_agent": "curl/8.0",
    "cache_status": "MISS"
  },
  {
    "logged_at_unix": 1721289602,
    "remote_addr": "198.51.100.21",
    "host": "www.example.com",
    "path": "/index.html",
    "status_code": 200,
    "bytes_sent": 8192,
    "request_length": 300,
    "request_time_ms": 8,
    "user_agent": "Mozilla/5.0",
    "cache_status": "HIT"
  }
]
FieldExplanation
bytes_sentdata provided (single request); global/Zone totals = Server sum
request_lengthdata received (single request)
logged_at_unixrequest completion time (business timeline)
hostused for Zone domain filtering
cache_status$upstream_cache_status as-is; detail/list can derive three states (hit/origin/un-cached); no upstream address reported
no regionwritten by Server at insert time via GeoIP

4.3 How the Server Uses It (product metrics)

Product MetricAlgorithm (L1 only)
Data providedsum(bytes_sent)
Data receivedsum(request_length)
Request countcount()
UVuniqExact(remote_addr)
Status distributiongroup by status_code
Top domainsgroup by host
Zone pagesame + host IN (that Zone's domains)
Dashboard business areasame, global or Top-filtered

Stored in: of_node_access_logs (optional Server-side of_access_log_hourly acceleration, Agent never writes it).

4.4 Report Frequency

text
Request happens ──immediately──► write access.log
Agent every 3s ──moves──► new lines in those 3s (possibly 0, possibly many)
Server ──immediately/batched──► CH

Business volume correctness does not depend on 3s alignment; 3s only affects "detail arrival latency at the control plane" and per-packet line count.


5. L2 Health: edge_health and /openflare/observability

5.1 Local Monitoring Endpoint

Data collection endpoint:

http
GET http://127.0.0.1:{openresty_observability_port}/openflare/observability

Default port: 18081 (openresty_observability_port).

Responsibility: answers "how is OpenResty right now", not "how much business data was provided".

Response Example

json
{
  "ok": true,
  "captured_at_unix": 1721289600,
  "connections": {
    "active": 42,
    "reading": 0,
    "writing": 1,
    "waiting": 41
  }
}
FieldInstant?SourceDescription
okthis probereturns 200 → trueliveness
captured_at_unixsampling timengx.time()aligned with report
connections.activeinstantNginx connection state (original stub_status Active)current active connections
reading / writing / waitinginstantsame, subdividedoptional but recommended

Not returned (removed):

Old FieldReason
request_count / error_count / UV / status_codes / top_domainsbusiness window summaries, now from access log
openresty_rx_bytes / openresty_tx_bytesduplicates data provided/received and error-prone
source_countriesnever implemented; countries go through Server GeoIP
server.accepts/handled/requestsprocess cumulative counters, easily confused with business requests; not on the main path

/openflare/stub_status: kept; /openflare/observability internally reads that endpoint to assemble the connection-count JSON, and the Agent health check also probes it directly.

5.2 Collection Mechanism (read snapshot)

text
Nginx maintains Active connections etc. on connect/disconnect

Agent GET /openflare/observability

only reads "current values" and returns JSON
  • No access.log scanning, no 60-second business averages.
  • Returns an instant gauge snapshot.

5.3 Report Example (packed into NodePayload)

json
"edge_health": {
  "captured_at_unix": 1721289600,
  "status": "healthy",
  "message": "",
  "connections": 42
}
FieldSource
status / messageAgent health probe (config validation/process etc., may work with the observability endpoint's ok); must align with top-level openresty_status / openresty_message
connectionsobservability endpoint connections.active

Storage split (authoritative sources):

ContentWritten To
latest status + messagePG node table (UI / list / alerts)
time-series status + connectionsCH of_node_edge_health (no message)

6. L3 Host: host_metrics

6.1 Where Collection Comes From

The Agent reads the local machine (e.g. /proc, disk stats), once per packet.

FieldSemanticsDescription
cpu_usage_percentinstantcurrent CPU%
memory_* / storage_*instant used/totalusage rates computed at Server or display layer
disk_read_bytes / disk_write_bytescumulative counterkernel cumulative IO
network_rx_bytes / network_tx_bytescumulative counterhost NIC, not data provided

6.2 Report Example

json
"host_metrics": {
  "captured_at_unix": 1721289600,
  "cpu_usage_percent": 12.5,
  "memory_used_bytes": 4294967296,
  "memory_total_bytes": 16106127360,
  "storage_used_bytes": 50000000000,
  "storage_total_bytes": 107374182400,
  "disk_read_bytes": 9000000000,
  "disk_write_bytes": 12000000000,
  "network_rx_bytes": 500000000000,
  "network_tx_bytes": 800000000000
}

6.3 How the Server Handles Cumulative Fields

text
store raw-value time series
when displaying "NIC outbound over this period":
  delta = current - previous
  if delta < 0 → treat as restart/counter reset, record this segment's increment as 0, continue from new baseline
  if delta >= 0 → record into that period's increment
  • The Agent reports raw values, never computes 24h totals at the edge.
  • Forbidden to sum cumulative raw values as business volume.
  • Copy must be "host NIC", never "data provided / OpenResty outbound".

Stored in: of_node_metric_snapshots (optional capacity hourly MV).


7. One Complete Report Example

json
{
  "schema_version": 2,
  "node_id": "n_01hxyz",
  "name": "edge-shanghai-1",
  "ip": "203.0.113.10",
  "version": "3.4.0",
  "ext_version": "",
  "current_version": "20260718-abc",
  "last_error": "",
  "host_metrics": {
    "captured_at_unix": 1721289600,
    "cpu_usage_percent": 12.5,
    "memory_used_bytes": 4294967296,
    "memory_total_bytes": 16106127360,
    "storage_used_bytes": 50000000000,
    "storage_total_bytes": 107374182400,
    "disk_read_bytes": 9000000000,
    "disk_write_bytes": 12000000000,
    "network_rx_bytes": 500000000000,
    "network_tx_bytes": 800000000000
  },
  "edge_health": {
    "captured_at_unix": 1721289600,
    "status": "healthy",
    "message": "",
    "connections": 42
  },
  "access_logs": [
    {
      "logged_at_unix": 1721289595,
      "remote_addr": "198.51.100.20",
      "host": "www.example.com",
      "path": "/",
      "status_code": 200,
      "bytes_sent": 4096,
      "request_length": 200,
      "request_time_ms": 12
    }
  ],
  "buffered": [],
  "health_events": [],
  "waf_ip_group_checksums": {
    "1": "d41d8cd98f00b204e9800998ecf8427e"
  }
}

Server storage sketch:

payload blockwritten to
access_logs[0]one CH row, bytes_sent=4096, region filled by GeoIP
edge_healthnode openresty_status=healthy, connections=42
host_metricsone CH metric row with cumulative/instant fields

Product query sketch (24h):

  • data provided = sum(bytes_sent) over that node's (or global) logs
  • current connections = latest edge_health.connections
  • host NIC outbound = sum of non-negative network_tx deltas over metrics

The three numbers need not be equal.


8. Offline Backfill buffered

When reporting fails, the Agent caches the same kind of facts locally by window (default ~60 minutes), then packs them into buffered[] after recovery:

json
"buffered": [
  {
    "captured_at_unix": 1721289500,
    "host_metrics": { },
    "edge_health": { },
    "access_logs": [ ]
  }
]
  • Only facts, no legacy TrafficReport.
  • Server processing logic is identical to the main fields.

9. End-to-End Timeline (default 3s)

text
t=0.0s   visitor request completes → writes one access.log line; connection count may change
t=0.1s   another request → another log line

t=3s     Agent heartbeat:
           · reads 2 access_logs lines
           · GET observability → connections=42
           · reads host_metrics
           · sends to Server
t=3s+    Server stores; dashboard/Zone queries aggregate logs
t=6s     next round…

10. Old Model Comparison

Old ApproachNew Model
Lua dict 60s window request_count + Agent 10s pull + Server sumremoved; request count = log count
openresty_tx as "outbound"removed; data provided = sum(bytes_sent)
Two endpoints observability + stub_statusdata collection unified through observability; stub_status kept as liveness and internal read endpoint
TrafficReport pre-aggregationremoved; no such path in protocol or API
Business and NIC both called "traffic"separate copy, separate APIs, separate tables
health status/messagePG latest-state authority; CH only status+connection time series

11. Config and Implementation Index

ItemLocation/Key
Heartbeat intervalAgent heartbeat_interval; control plane agent_heartbeat_interval (default 3000ms)
Offline thresholdcontrol plane node_offline_threshold (default 60000ms)
Observability portopenresty_observability_port (default 18081)
access.log pathaccess_log_path
Replay minutesobservability_replay_minutes (default 60)
Protocol typespkg/protocol/agent.go (evolves to v2 when landing)
Table DDLobservability-data-model.md

12. Revision History

DateNotes
2026-07-18initial draft: single-page "latest transport model" — three layers, frequency, sample JSON, collection sources, old-model comparison
2026-07-18default report interval 3s; offline threshold 60s; replay window 60 minutes
2026-07-18M5: edge_health table, access_log_hourly, deprecate request_reports/obs_openresty throughput tables
2026-07-18no compatibility layer: removed "may ignore during compat period" wording; health message only in PG, no message in CH

Released under the Apache License 2.0