Skip to content

Edge Observability and Business Traffic Statistics Refactor

You will learn: the problems this refactor solves (dashboard "OpenResty outbound" vs "Zone data provided" inconsistency, field and aggregation redundancy), and how the target architecture makes the Agent report only facts and the Server interpret facts, with access logs as the single source of truth for business traffic.


1. Goals

1.1 Problems to Solve

  1. Dual sources of truth: business throughput comes from both access-log aggregation and OpenResty observability deltas, and the numbers never match long-term.
  2. Agent over-computes: the edge pre-aggregates TrafficReport, throughput accumulation, and the control plane aggregates again — semantics are hard to evolve and reconcile.
  3. Field semantic overlap: "OpenResty outbound" and "data provided" are the same business problem for users, but the system uses two field sets and two pipelines.
  4. Instant vs cumulative mixed: 60-second window counts are treated as process cumulative values for 24h deltas, causing severe underestimation.
  5. UI induces wrong comparisons: the dashboard and Zone page use similar "traffic/data" wording without declaring scope and caliber differences.

1.2 Refactor Goals

GoalDescription
Single business truthrequest count, data provided, UV, status distribution, Top domains etc. only derived from access logs (and Server-side rollups)
Agent reports only factsdetail logs + machine readings + health snapshots; business UV/TopN/24h totals pre-aggregation is forbidden
Field convergenceone business concept maps to one authoritative field; machine NIC and business delivery strictly separated by name
Reconcilableglobal "data provided" ≈ sum of per-Zone "data provided" (difference only from unbound/unknown Hosts)
Evolvablechanging time windows, TopN, ownership rules only changes the Server, not the Agent

1.3 Non-Goals (outside this design)

  • Building a general log platform, full-log long-term archive, or search product.
  • Replacing ClickHouse / removing the analytics DB dependency.
  • Reworking Relay / OpenFlared host metric collection (principles align, but not in this round's protocol main path).
  • Real-time streaming alert engine, APM tracing (the OpenTelemetry server side already exists and is orthogonal to this business traffic model).

2. Scope and Constraints

2.1 Product Constraints (inherited)

  • Single-tenant, single globally active config; observability introduces no multi-tenant billing isolation.
  • Access logs and time-series observability use the switchable log primary DB (ClickHouse by default; switchable to PostgreSQL/SQLite), see Log Store Decoupling.
  • Agent has no inbound control, Pull model; during offline periods local OpenResty keeps serving, and observability can buffer locally and backfill.

2.2 Engineering Constraints

  • Agent stays lightweight: parse log lines, read /proc, health checks; no business analysis.
  • Control-plane API errors still use the unified envelope and response.Abort*.
  • Access log field changes must update both the OpenResty log_format and the Agent parser simultaneously; Agent and control plane ship at the same version, no legacy protocol parsing.

3. Design Principles

Principle P1: Agent Reports Facts, Server Interprets Facts

text
Agent  = collection + reliable delivery (raw / near-raw)
Server = storage + aggregation + ownership + trends + reconciliation

Allowed edge processing (collection)

  • Parsing JSON access.log lines into structured fields
  • path length caps, dropping invalid lines, skipping observability-port's own requests
  • Reading NIC/CPU/memory counters as raw values
  • Batching, compression, offline buffering and retries

Forbidden edge processing (business computation)

  • UV / Top domains / status histograms / window request_count as authoritative metrics
  • Maintaining "business in/out cumulative" for the dashboard
  • Zone / domain ownership stats, country distribution (country can be resolved at Server insert time)

Principle P2: Single Truth for Business Traffic = Access Logs

Business QuestionSingle Answer
How much data was providedsum(bytes_sent)
How many requestscount()
How many unique visitorsuniqExact(remote_addr) (or product-defined hashing)
Status codes / Top domainsgroup by on logs

Principle P3: Three Metric Layers Never Mixed

LayerNamePurposeTypical Fields
L1 Business deliveryBusiness Trafficuser & Zone reconciliation, dashboard business trendsaccess log
L2 Edge healthEdge Healthis OpenResty alive, current connectionsstatus, connections
L3 Host capacityHost Capacitycapacity planning, is the machine saturatedCPU, memory, disk, NIC

Never name L3 NIC or L2 instant counts as "data provided"; never draw L1 and L3 on the same summary card without labeling semantics.

Principle P4: One Business Concept, One Field

  • Data provided ≡ response body delivered ≡ "OpenResty outbound (business meaning)" in legacy copy → keep only bytes_sent aggregation
  • Data received (optional) ≡ request-side volume → log request_length aggregation
  • Host outboundnetwork_tx delta, copy must include "host/NIC"

4. Pre-Refactor Problems (Baseline)

4.1 Pre-Refactor Data Flow (redundant)

text
One HTTP request

  ├─ access.log line
  │     → Agent tail → AccessLogs[]
  │     → CH of_node_access_logs
  │     → Zone "data provided" ✅

  ├─ Lua shared dict window/cumulative counts
  │     → /openflare/observability
  │     → TrafficReport + OpenrestyObservation(rx/tx)
  │     → CH request_reports / obs_openresty
  │     → dashboard "OpenResty in/outbound" ❌ easily inconsistent with Zone

  ├─ second access.log aggregation (fallback when observability endpoint fails)
  │     → yet another TrafficReport / throughput

  └─ host network_rx/tx
        → Snapshot → "host" curve in network trends

4.2 Field Overlap

User PerceptionSystem Field ASystem Field BProblem
Outbound / providedopenresty_tx_bytesbytes_sentduplicate business semantics
Inboundopenresty_rx_bytesrequest_length (log)duplicate business semantics
Request countTrafficReport.request_countcount(access_logs)duplicate aggregation, window easily double-counted
Outbound (machine)network_tx_bytes(no business equivalent)should be named separately, never reconciled with business

4.3 Typical Failure Modes

  1. Window counts treated as cumulative deltas → 24h business throughput severely underestimated.
  2. Hourly rollup max−min broken for resetting counters.
  3. Zone uses logs, dashboard uses observability → users think the system is wrong.
  4. Changing caliber requires syncing Lua, Agent state accumulation, Server deltas, and frontend copy.

5. Target Architecture

5.1 Target Data Flow

mermaid
flowchart TB
  subgraph edge [Edge Node]
    OR[OpenResty]
    LOG[access.log]
    PROC[host /proc and disk]
    STUB[stub_status connections]
    AG[Agent]
    OR -->|log_format writes line| LOG
    LOG -->|tail incremental details only| AG
    PROC -->|reading snapshots| AG
    STUB -->|instant connections| AG
    OR -->|health probe| AG
  end

  subgraph server [Control-Plane Server]
    HB[Heartbeat / WS receive]
    CH[(ClickHouse)]
    AGG[Aggregation query layer]
    API[Admin API]
    HB --> CH
    CH --> AGG
    AGG --> API
  end

  subgraph ui [Admin Panel]
    DASH[Dashboard: global business trends]
    ZONE[Zone: filter by domain]
    NODE[Node: host resources + health]
  end

  AG -->|AccessLogs + HostSnapshot + Health| HB
  API --> DASH
  API --> ZONE
  API --> NODE

5.2 Responsibility Matrix

CapabilityAgentServerFrontend
Write access.logOpenResty
Read and report detailsstore
sum/count/uniq/TopNdisplay
Zone domain filteringselect Zone
Host CPU/memory/NICread raw values and reportdelta/averagenode/dashboard resource area
OpenResty connectionsread instant and reportlatest valuenode health
Business 24h in/outboundlog aggregationuniformly called "data provided/received"

6. Metrics and Field Model

6.1 Authoritative Field Table (target)

L1 Business Delivery (from access logs)

ConceptStorage FieldAggregationDisplay Name
Request timelogged_atwindow filter
Nodenode_idgroup
Client IPremote_addruniq → UVUnique visitors
Hosthostgroup / Zone mappingDomain
Pathpathoptional
Status codestatus_codegroupStatus distribution
Data providedbytes_sentsumData provided
Data receivedrequest_lengthsumData received (optional display)
Regionregion (resolved & written by Server)groupSource region

Note: the JSON key in the OpenResty log_format may keep the name bytes_sent; the value must come from $body_bytes_sent (consistent with production), representing response body delivered, i.e. "data provided".

L2 Edge Health (instant; no 24h business totals)

ConceptFieldDescription
OpenResty healthopenresty_status / messageexisting
Current connectionsopenresty_connectionsstub_status
(optional) rough recent-window QPSnode detail "right now" only, never authoritative 24h totalsif implemented must be labeled "instant"

L3 Host Capacity

ConceptFieldDisplay Name
CPU / memory / disk usagehost_metricskeep
NIC cumulative bytesnetwork_rx_bytes / network_tx_bytesHost NIC in/outbound
Disk IO cumulativedisk_read_bytes / disk_write_bytesDisk read/write

6.2 Removed Fields (no compatibility layer)

Original FieldDispositionReason
openresty_tx_bytes / openresty_rx_bytesremovedbusiness bytes follow access logs
TrafficReport and TopN/window UVremovededge pre-aggregation
Agent state business lifetime accumulatorsremovedviolates P1
Lua shared dict business throughput/window request countsremovednot the delivery main path

6.3 Naming Reference (frontend copy enforced)

Forbidden CopyCorrect CopyData Source
OpenResty outbound (business volume)Data providedsum(bytes_sent)
OpenResty inbound (business volume)Data receivedsum(request_length)
Network outbound (unspecified)Host NIC outboundnetwork_tx delta
Two cards: data provided vs outboundkeep only one business cardlogs

7. Agent Design

7.1 Heartbeat Payload (target protocol)

Keep and strengthen:

text
NodePayload
  identity / version / openresty_status / openresty_message  # latest state → PG
  profile                  # host overview (low frequency)
  host_metrics             # L3 resource readings (incl. NIC cumulative raw values)
  edge_health              # L2: status + connections (CH time series; message not in CH)
  access_logs[]            # L1 details (main path)
  health_events[]
  buffered[]               # buffered facts above, not reports
  waf_ip_group_checksums

Removed from the protocol (no compatibility layer):

text
traffic_report
openresty_observation
snapshot / buffered_observability aliases

7.2 Access Log Reporting Requirements

Each detail at minimum contains:

FieldRequiredNote
logged_at_unixrequest completion time
remote_addrUV
hostZone mapping
pathmay be truncated
status_code
bytes_sentbody bytes, data provided
request_lengthdata received

Agent responsibilities:

  1. Tail access.log by offset (reset offset on truncation/rotation, only report new lines still present in the file).
  2. Parse into structured form, batch into heartbeat / WS.
  3. Offline writes to local buffer, backfill by window once connected.
  4. No sum/count/uniq on details.

7.3 Host Snapshot

  • Keep reporting NIC/disk cumulative counter raw values (not business pre-aggregation).
  • Server does non-negative deltas between adjacent samples → host trends.
  • This is unrelated to "data provided"; the UI must display it in a separate section.

7.4 OpenResty Local Observability

Converged state:

  • Keep: health checks, stub_status current connections.
  • The main path no longer relies on log.lua shared dict business counts; /openflare/observability only returns health and connection snapshots, not business report sources.

7.5 Relationship with the Agent Design Doc

This design strengthens "pure data landing" in Agent & Publish Model:

  • Config and certificates: land and report applied state.
  • Observability: only carry facts, not business conclusions.

8. Server Design

8.1 Storage

InputTableDescription
access_logs[]of_node_access_logsauthoritative business details
host_metricsof_node_metric_snapshotsL3; NIC/disk cumulative
openresty_status / openresty_messagePG node tableL2 latest-state authority (message only here)
edge_healthof_node_edge_healthL2 time series: status + connections (no message)

GeoIP: continue resolving remote_addrregion in the Server insert path, not in the Agent.

8.2 Aggregation Layer (unified)

All business trends and Zone stats share the same query semantics:

text
filter: logged_at ∈ [since, until]
optional: node_id / host IN (...)
metrics:
  request_count     = count()
  unique_visitors   = uniqExact(remote_addr)
  bytes_provided    = sum(bytes_sent)      -- data provided
  bytes_received    = sum(request_length)  -- data received
  series folded by hour/bucket
  distributions by status_code / host / region

Implementation locations:

  • Zone: GET .../zones/:id/stats (existing, align field naming)
  • Dashboard: overview traffic / business network trends switch to the same aggregation (global, no host filter or Top filter)
  • Node detail: business volume = the same aggregation filtered by that node_id; host NIC still uses metric deltas

8.3 Derived Rollups (optional performance path)

When detail queries over all nodes for 24h are too heavy, allow Server-side materialized views:

text
of_access_log_hourly
  (hour, node_id, host, request_count, bytes_sent, bytes_received, ...)

Constraints:

  • Derived only by CH from of_node_access_logs; Agent is forbidden from writing this table directly.
  • Zone / dashboard prefer reading the rollup, falling back to details (similar to the existing metric hourly policy).

8.4 Decommissioned Analytics Paths

PathAfter Migration
BuildNetworkTrendPoints delta on openresty_rx/txdeleted, or keep only network_* host curves
of_node_obs_openresty throughput fieldsstop writing; drop table or shrink columns after TTL expiry
of_node_request_reports + traffic hourlybusiness trends no longer depend on it; table can be deprecated wholesale
Dashboard compact openresty_tx serieschange to bytes_provided series

9. API and Frontend

9.1 Semantically Unified Response Fields

Business stats APIs should uniformly use:

json
{
  "request_count": 0,
  "unique_visitors": 0,
  "bytes_provided": 0,
  "bytes_received": 0,
  "series": [
    {
      "bucket_started_at": "...",
      "request_count": 0,
      "unique_visitors": 0,
      "bytes_provided": 0,
      "bytes_received": 0
    }
  ]
}

API business byte fields use bytes_provided / bytes_received (access-log aggregation); no more openresty throughput aliases.

9.2 Dashboard

  • Business area: request trend, data provided, data received (optional), status codes, Top domains, source regions — all L1.
  • Resource area: CPU/memory, host NIC, disk IO — all L3.
  • Forbidden: showing "OpenResty in/outbound" in the business area as a metric reconciled with Zone.

Suggest splitting or retitling "24-hour network and disk trends":

  • "24-hour business traffic" → bytes_provided / bytes_received / requests
  • "24-hour host network and disk" → network_* / disk_*

9.3 Zone /websites/:id

  • Keep cards like "total data provided".
  • Data and the dashboard business area use the same aggregation function, only hosts = zone domain list.
  • Docs and UI may note: the global dashboard includes all Hosts; this page is only this Zone.

9.4 Node Detail

  • Business throughput: that node's sum(bytes_sent) etc.
  • OpenResty: health + current connections.
  • NIC: clearly "host".

10. OpenResty and Log Format

10.1 Keep

Existing JSON log_format core fields:

text
ts, host, path, remote_addr, status, request_time,
bytes_sent (= $body_bytes_sent), request_length

10.2 Changes

  • No longer rely on log-phase writes of business shared dict counts as control-plane input.
  • Observability-port requests continue not writing business stats (or access_log off).

10.3 Agent Parsing

  • Protocol NodeAccessLog adds request_length.
  • Legacy log lines missing fields default to 0, not blocking the whole batch.

11. Upgrade and Migration (no compatibility layer)

11.1 Phase Review (shipped)

PhaseContent
M1–M5read path switches to access logs; protocol v2; stop pre-aggregation; edge_health + access_log_hourly; drop old tables and API compat fields

11.2 Upgrade Strategy

  • Agent: destroy-and-recreate preferred; binary replacement allowed.
  • On binary replacement: the local old observability buffer (including snapshot / openresty_observation / traffic_report) is deleted wholesale, rebuilt after running.
  • Server does not parse v1 fields, does not dual-read request_reports / openresty throughput.
  • Detail-missing periods: business charts are empty or partial; never impersonate data provided with NIC or removed openresty throughput.

11.3 Data Backfill

  • Historical "data provided" follows access logs.
  • Before of_access_log_hourly is created, history is backfilled with goose SQL (ANTI JOIN to prevent duplicates).

11.4 Health-State Authority

  • Current state: PG openresty_status / openresty_message.
  • Time series: log primary DB of_node_edge_health (status + connections; no message).

11.5 UV

  • Whole-window unique visitors: uniqExact(remote_addr) (dashboard totals, Zone totals).
  • Bucketed UV (Zone curves): per-bucket uniq, not summable across buckets; UI must note it.
  • Hourly trend path: don't plot / fill per-hour UV (hourly table has no UV).

12. Storage and Capacity

  • Business trends rely on details or hourly rollups; watch of_node_access_logs TTL and sampling.
  • If details are too large: prefer Server-side rollup rather than restoring Agent pre-aggregation.
  • For high-cardinality path scenarios, limit detail path length (existing); aggregation doesn't do global Top over full paths by default.

13. Risks and Trade-offs

RiskMitigation
Large detail volume makes CH and heartbeat heavybatching, compression, sampling policy evaluation; Server rollup; limit per-batch count
Brief log loss lowers business volumelocal buffer and rotation handling; monitor access log collection lag
Users still compare "NIC outbound" with "data provided"UI sections and copy enforce the "host" prefix
Old Agents stay online long-termno compatibility layer; Agents must be upgraded/rebuilt

Why not keep Agent pre-aggregation as an optimization?

  • Saving bandwidth re-splits the truth, drifts calibers, and repeats this problem.
  • Optimization belongs in Server derived tables and queries, not edge business computation.

14. Key Decision Summary

DecisionChoiceRejected Alternative
Business traffic truthaccess logsOpenResty dict / TrafficReport
Agent rolereport only factsedge UV/TopN/throughput accumulation
"Outbound" vs "provided"merged into data provideddual fields and dual pipelines long-term
NIC trafficindependent L3, separate copyreconciled side-by-side with business outbound
PerformanceCH rollupAgent pre-aggregation
Migrationswitch read path first, then slim Agentdrop details first, rely on pre-aggregation

15. Doc and Code Mapping

AreaMain Paths
Protocolpkg/protocol/agent.go
Agent collectioninternal/apps/agent/observability/, heartbeat/
OpenResty logging and Luapkg/render/openresty/, internal/apps/agent/nginx/observability_assets.go
Server storageinternal/apps/openflare/agent/observability.go
Log aggregationinternal/repository/analytics/node_access_log*.go, internal/apps/openflare/zone/stats.go
Dashboardinternal/apps/openflare/dashboard/, internal/apps/openflare/observability/analytics.go
Frontendfrontend/app/(main)/page.tsx, components/dashboard/*, websites/.../zone-overview.tsx

Recommended reading order:

  1. Observability Transport Model (latest: what to send, where collected from, frequency, sample JSON)
  2. Agent Reporting Protocol and Observability Data Model (protocol fields and DDL)

16. Revision History

DateNotes
2026-07-17initial draft: target architecture and migration phases for dual truth, Agent pre-aggregation, field redundancy
2026-07-17added protocol/table-structure chapter links observability-data-model.md

Released under the Apache License 2.0