mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 23:06:36 +08:00
feat: enhance observability metrics collection and reporting
- Updated BuildSnapshot function to include OpenResty metrics. - Enhanced BuildTrafficReport to utilize managed OpenResty metrics. - Introduced new functions for parsing access logs in both JSON and combined formats. - Added tests for traffic report generation from combined access logs. - Implemented local observability metrics collection from OpenResty. - Created Lua scripts for OpenResty to gather observability data. - Updated configuration documentation to include new observability port and settings. - Added support for OpenResty observability in the server configuration.
This commit is contained in:
@@ -115,9 +115,11 @@ type configBundle struct {
|
||||
}
|
||||
|
||||
const (
|
||||
nginxCertDirPlaceholder = "__ATSF_CERT_DIR__"
|
||||
nginxRouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__"
|
||||
nginxAccessLogPlaceholder = "__ATSF_ACCESS_LOG__"
|
||||
nginxCertDirPlaceholder = "__ATSF_CERT_DIR__"
|
||||
nginxRouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__"
|
||||
nginxAccessLogPlaceholder = "__ATSF_ACCESS_LOG__"
|
||||
nginxLuaDirPlaceholder = "__ATSF_LUA_DIR__"
|
||||
nginxObservabilityPortPlaceholder = "__ATSF_OBSERVABILITY_PORT__"
|
||||
)
|
||||
|
||||
var requiredMainConfigTemplatePlaceholders = []string{
|
||||
@@ -351,6 +353,7 @@ func buildCurrentConfigBundle(requireRoutes bool) (*configBundle, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
supportFiles = append(supportFiles, buildOpenRestyObservabilitySupportFiles()...)
|
||||
mainConfig := renderMainConfig(openRestyConfig)
|
||||
return &configBundle{
|
||||
Routes: routes,
|
||||
@@ -675,17 +678,21 @@ func renderTemplateDirective(enabled bool, statement string) string {
|
||||
}
|
||||
|
||||
func renderOpenRestyCacheTemplateBlock(cfg openRestyConfigSnapshot) string {
|
||||
lines := make([]string, 0, 8)
|
||||
if !cfg.CacheEnabled {
|
||||
return ""
|
||||
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
return strings.Join([]string{
|
||||
lines = append(lines, strings.Join([]string{
|
||||
fmt.Sprintf(" proxy_cache_path %s levels=%s keys_zone=atsflare_cache:10m inactive=%s max_size=%s;", cfg.CachePath, cfg.CacheLevels, cfg.CacheInactive, cfg.CacheMaxSize),
|
||||
fmt.Sprintf(" proxy_cache_key \"%s\";", cfg.CacheKeyTemplate),
|
||||
fmt.Sprintf(" proxy_cache_lock %s;", onOff(cfg.CacheLockEnabled)),
|
||||
fmt.Sprintf(" proxy_cache_lock_timeout %s;", cfg.CacheLockTimeout),
|
||||
fmt.Sprintf(" proxy_cache_use_stale %s;", cfg.CacheUseStale),
|
||||
"",
|
||||
}, "\n")
|
||||
}, "\n"))
|
||||
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
|
||||
func onOff(value bool) string {
|
||||
|
||||
@@ -57,6 +57,12 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) {
|
||||
if !strings.Contains(result.Version.MainConfig, "access_log __ATSF_ACCESS_LOG__ atsflare_json;") {
|
||||
t.Fatal("expected main config to include managed access log placeholder")
|
||||
}
|
||||
if !strings.Contains(result.Version.MainConfig, "log_by_lua_file __ATSF_LUA_DIR__/observability/log.lua;") {
|
||||
t.Fatal("expected main config to include managed openresty lua log hook")
|
||||
}
|
||||
if !strings.Contains(result.Version.MainConfig, "listen 127.0.0.1:__ATSF_OBSERVABILITY_PORT__;") {
|
||||
t.Fatal("expected main config to include managed openresty observability port placeholder")
|
||||
}
|
||||
if !strings.Contains(result.Version.RenderedConfig, "listen 443 ssl;") {
|
||||
t.Fatal("expected rendered config to include https server block")
|
||||
}
|
||||
@@ -69,6 +75,9 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) {
|
||||
if !strings.Contains(result.Version.SupportFilesJSON, ".crt") || !strings.Contains(result.Version.SupportFilesJSON, ".key") {
|
||||
t.Fatal("expected support files to contain certificate and key")
|
||||
}
|
||||
if !strings.Contains(result.Version.SupportFilesJSON, "observability/log.lua") || !strings.Contains(result.Version.SupportFilesJSON, "observability/read.lua") {
|
||||
t.Fatal("expected support files to contain managed openresty observability lua scripts")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyRouteRejectsHTTPSWithoutCertificate(t *testing.T) {
|
||||
@@ -187,6 +196,9 @@ func TestPreviewAndDiffConfigVersion(t *testing.T) {
|
||||
if !strings.Contains(preview.MainConfig, "include __ATSF_ROUTE_CONFIG__;") {
|
||||
t.Fatal("expected preview main config to include managed route config placeholder")
|
||||
}
|
||||
if !strings.Contains(preview.MainConfig, "log_by_lua_file __ATSF_LUA_DIR__/observability/log.lua;") {
|
||||
t.Fatal("expected preview main config to include managed openresty lua log hook")
|
||||
}
|
||||
if !strings.Contains(preview.RenderedConfig, `proxy_set_header X-Release "candidate";`) {
|
||||
t.Fatal("expected preview config to include modified custom header")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package service
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
openRestyObservabilitySupportDir = "observability"
|
||||
openRestyObservabilityInitLuaPath = openRestyObservabilitySupportDir + "/init.lua"
|
||||
openRestyObservabilityLogLuaPath = openRestyObservabilitySupportDir + "/log.lua"
|
||||
openRestyObservabilityReadLuaPath = openRestyObservabilitySupportDir + "/read.lua"
|
||||
openRestyObservabilityWindowTTL = 7200
|
||||
)
|
||||
|
||||
const openRestyObservabilityInitLua = `local dict = ngx.shared.atsflare_observability
|
||||
if not dict then
|
||||
return
|
||||
end
|
||||
|
||||
local now = ngx.time()
|
||||
local current_window = dict:get("current_window")
|
||||
if not current_window then
|
||||
dict:set("current_window", now)
|
||||
dict:set("window_started_at:" .. now, now)
|
||||
end
|
||||
`
|
||||
|
||||
const openRestyObservabilityLogLua = `local dict = ngx.shared.atsflare_observability
|
||||
if not dict then
|
||||
return
|
||||
end
|
||||
|
||||
local ttl = ` + "7200" + `
|
||||
local current_window = dict:get("current_window")
|
||||
local now = ngx.time()
|
||||
if not current_window then
|
||||
current_window = now
|
||||
dict:set("current_window", current_window)
|
||||
dict:set("window_started_at:" .. current_window, now)
|
||||
end
|
||||
|
||||
local function ensure_counter(key)
|
||||
dict:add(key, 0, ttl)
|
||||
end
|
||||
|
||||
local function incr(key, delta)
|
||||
ensure_counter(key)
|
||||
local value, err = dict:incr(key, delta)
|
||||
if not value and err == "not found" then
|
||||
dict:set(key, delta, ttl)
|
||||
end
|
||||
end
|
||||
|
||||
local function remember_value(list_key, marker_key, value)
|
||||
if value == "" then
|
||||
return
|
||||
end
|
||||
if not dict:add(marker_key, 1, ttl) then
|
||||
return
|
||||
end
|
||||
local existing = dict:get(list_key)
|
||||
if not existing or existing == "" then
|
||||
dict:set(list_key, value, ttl)
|
||||
return
|
||||
end
|
||||
dict:set(list_key, existing .. "\n" .. value, ttl)
|
||||
end
|
||||
|
||||
local window_prefix = tostring(current_window)
|
||||
incr("request_count:" .. window_prefix, 1)
|
||||
|
||||
local status = tostring(ngx.status or 0)
|
||||
if status ~= "0" then
|
||||
incr("status:" .. window_prefix .. ":" .. status, 1)
|
||||
remember_value(
|
||||
"status_keys:" .. window_prefix,
|
||||
"status_marker:" .. window_prefix .. ":" .. status,
|
||||
status
|
||||
)
|
||||
if tonumber(status) and tonumber(status) >= 500 then
|
||||
incr("error_count:" .. window_prefix, 1)
|
||||
end
|
||||
end
|
||||
|
||||
local host = tostring(ngx.var.host or "")
|
||||
if host ~= "" then
|
||||
incr("domain:" .. window_prefix .. ":" .. host, 1)
|
||||
remember_value(
|
||||
"domain_keys:" .. window_prefix,
|
||||
"domain_marker:" .. window_prefix .. ":" .. host,
|
||||
host
|
||||
)
|
||||
end
|
||||
|
||||
local remote_addr = tostring(ngx.var.binary_remote_addr or ngx.var.remote_addr or "")
|
||||
if remote_addr ~= "" and dict:add("visitor:" .. window_prefix .. ":" .. remote_addr, 1, ttl) then
|
||||
incr("unique_visitor_count:" .. window_prefix, 1)
|
||||
end
|
||||
|
||||
local request_length = tonumber(ngx.var.request_length) or 0
|
||||
if request_length > 0 then
|
||||
incr("openresty_rx_bytes:" .. window_prefix, request_length)
|
||||
end
|
||||
|
||||
local bytes_sent = tonumber(ngx.var.bytes_sent) or tonumber(ngx.var.body_bytes_sent) or 0
|
||||
if bytes_sent > 0 then
|
||||
incr("openresty_tx_bytes:" .. window_prefix, bytes_sent)
|
||||
end
|
||||
`
|
||||
|
||||
const openRestyObservabilityReadLua = `local cjson = require "cjson.safe"
|
||||
|
||||
local dict = ngx.shared.atsflare_observability
|
||||
if not dict then
|
||||
ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
|
||||
ngx.say(cjson.encode({ message = "shared dict unavailable" }))
|
||||
return
|
||||
end
|
||||
|
||||
local now = ngx.time()
|
||||
local current_window = dict:get("current_window")
|
||||
if not current_window then
|
||||
current_window = now
|
||||
dict:set("current_window", current_window)
|
||||
dict:set("window_started_at:" .. current_window, now)
|
||||
end
|
||||
|
||||
local function read_counter(key)
|
||||
return tonumber(dict:get(key) or 0) or 0
|
||||
end
|
||||
|
||||
local function read_map(window_id, prefix, list_key)
|
||||
local result = {}
|
||||
local raw = dict:get(list_key .. ":" .. window_id)
|
||||
if not raw or raw == "" then
|
||||
return result
|
||||
end
|
||||
for value in string.gmatch(raw, "[^\n]+") do
|
||||
result[value] = read_counter(prefix .. ":" .. window_id .. ":" .. value)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local payload = {
|
||||
window_started_at_unix = read_counter("window_started_at:" .. current_window),
|
||||
window_ended_at_unix = now,
|
||||
request_count = read_counter("request_count:" .. current_window),
|
||||
error_count = read_counter("error_count:" .. current_window),
|
||||
unique_visitor_count = read_counter("unique_visitor_count:" .. current_window),
|
||||
status_codes = read_map(current_window, "status", "status_keys"),
|
||||
top_domains = read_map(current_window, "domain", "domain_keys"),
|
||||
source_countries = {},
|
||||
openresty_rx_bytes = read_counter("openresty_rx_bytes:" .. current_window),
|
||||
openresty_tx_bytes = read_counter("openresty_tx_bytes:" .. current_window)
|
||||
}
|
||||
|
||||
local next_window = now
|
||||
if next_window <= current_window then
|
||||
next_window = current_window + 1
|
||||
end
|
||||
dict:set("current_window", next_window)
|
||||
dict:set("window_started_at:" .. next_window, now)
|
||||
|
||||
ngx.header.content_type = "application/json"
|
||||
ngx.say(cjson.encode(payload))
|
||||
`
|
||||
|
||||
func buildOpenRestyObservabilitySupportFiles() []SupportFile {
|
||||
return []SupportFile{
|
||||
{Path: openRestyObservabilityInitLuaPath, Content: openRestyObservabilityInitLua},
|
||||
{Path: openRestyObservabilityLogLuaPath, Content: openRestyObservabilityLogLua},
|
||||
{Path: openRestyObservabilityReadLuaPath, Content: openRestyObservabilityReadLua},
|
||||
}
|
||||
}
|
||||
|
||||
func renderOpenRestyObservabilityTemplateBlock() string {
|
||||
return stringsJoinLines(
|
||||
" lua_shared_dict atsflare_observability 10m;",
|
||||
fmt.Sprintf(" init_worker_by_lua_file %s/%s;", nginxLuaDirPlaceholder, openRestyObservabilityInitLuaPath),
|
||||
fmt.Sprintf(" log_by_lua_file %s/%s;", nginxLuaDirPlaceholder, openRestyObservabilityLogLuaPath),
|
||||
"",
|
||||
fmt.Sprintf(" server {"),
|
||||
fmt.Sprintf(" listen 127.0.0.1:%s;", nginxObservabilityPortPlaceholder),
|
||||
" server_name atsflare-observability;",
|
||||
" access_log off;",
|
||||
" allow 127.0.0.1;",
|
||||
" deny all;",
|
||||
"",
|
||||
" location = /atsflare/observability {",
|
||||
" default_type application/json;",
|
||||
fmt.Sprintf(" content_by_lua_file %s/%s;", nginxLuaDirPlaceholder, openRestyObservabilityReadLuaPath),
|
||||
" }",
|
||||
"",
|
||||
" location = /atsflare/stub_status {",
|
||||
" stub_status;",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
func stringsJoinLines(lines ...string) string {
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := ""
|
||||
for index, line := range lines {
|
||||
if index > 0 {
|
||||
result += "\n"
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result + "\n"
|
||||
}
|
||||
@@ -1350,7 +1350,6 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
</p>
|
||||
{node.latest_apply_checksum ? (
|
||||
<div className="space-y-1 text-sm text-[var(--foreground-secondary)]">
|
||||
<p>目标 Checksum:{node.latest_apply_checksum}</p>
|
||||
<p>支持文件:{node.latest_support_file_count}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user