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