openship 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bare-FSUAM5V5.js → bare-IWS2QHJ3.js} +1 -1
- package/dist/{chunk-VE2YOK4I.js → chunk-2NZSUXOB.js} +25 -4
- package/dist/{chunk-EP2TNALB.js → chunk-3NZICQHF.js} +1 -1
- package/dist/{chunk-QEH4QUBX.js → chunk-EW6KB2J7.js} +1 -1
- package/dist/{chunk-GUQ3QCJ7.js → chunk-IOT5ZX23.js} +4 -4
- package/dist/{chunk-PSZSKEPA.js → chunk-RRJJGVKE.js} +1 -1
- package/dist/{chunk-B3CE2OMR.js → chunk-UC7YIEHT.js} +22 -5
- package/dist/{chunk-OGY63DGD.js → chunk-YIPMDGG6.js} +3 -5
- package/dist/{cloud-E5M6VMCG.js → cloud-M2YLWEXO.js} +5 -5
- package/dist/{edge-import-CQWHN2Q3.js → edge-import-SAYTRJLO.js} +7 -7
- package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-CFSCPGZK.js} +3 -3
- package/dist/{executor-JELARVKC.js → executor-PT6UEJ5M.js} +1 -1
- package/dist/index.js +73 -19
- package/dist/{nginx-AXGNWRKZ.js → nginx-YOEEFBTE.js} +2 -2
- package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-WGBO2M3Z.js} +1 -1
- package/dist/server/index.js +115 -32
- package/dist/server/lua/geo_country.lua +4 -3
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +22 -24
- package/dist/{setup-FA4PW4NU.js → setup-M2ZDGH3O.js} +4 -4
- package/package.json +1 -1
- package/dist/server/lua/pipe_log.lua +0 -77
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
-- image from apps/api/assets/geoip, so no runtime download)
|
|
15
15
|
--
|
|
16
16
|
-- Falls back gracefully - if the library or database is missing,
|
|
17
|
-
-- get_country_code() returns nil.
|
|
18
|
-
--
|
|
19
|
-
-- the
|
|
17
|
+
-- get_country_code() returns nil. site_logger.lua wraps this module in
|
|
18
|
+
-- pcall(require, ...) so a missing DB never crashes the request pipeline
|
|
19
|
+
-- (the country it resolves flows into the ring buffer, and from there to
|
|
20
|
+
-- both /logs/recent and the live SSE stream). mgmt_api `GET /status` reports whether geo actually
|
|
20
21
|
-- resolved, so a silently-dark edge is visible instead of just yielding an
|
|
21
22
|
-- empty country map.
|
|
22
23
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
-- content_by_lua: SSE endpoint for real-time request log streaming.
|
|
3
3
|
-- GET /logs/stream?domain=example.com
|
|
4
4
|
-- Internal only - 127.0.0.1:9145
|
|
5
|
+
--
|
|
6
|
+
-- Reads the SAME ring buffer site_logger.lua writes (rlog:{domain}:seq +
|
|
7
|
+
-- rlog:{domain}:{slot}), by a PER-CONNECTION cursor. Nothing is consumed: N
|
|
8
|
+
-- concurrent watchers each walk the ring independently and none steals another's
|
|
9
|
+
-- frames. This is what replaced the old single-subscriber log_pipe:q queue —
|
|
10
|
+
-- a containerized edge tails this over `docker exec curl`, and a curl that
|
|
11
|
+
-- outlives its client (the daemon buffers its stdout, so no EPIPE) can no longer
|
|
12
|
+
-- drain the queue out from under the next reader.
|
|
5
13
|
|
|
6
14
|
local sh = ngx.shared.request_data
|
|
7
15
|
if not sh then
|
|
@@ -20,12 +28,10 @@ end
|
|
|
20
28
|
domain = domain:lower()
|
|
21
29
|
if domain:sub(1, 4) == "www." then domain = domain:sub(5) end
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
local
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
sh:delete(QUEUE_KEY)
|
|
28
|
-
sh:set(SUB_KEY, true, 30)
|
|
31
|
+
-- MUST match site_logger.lua's RING — same slot arithmetic on both ends.
|
|
32
|
+
local RING = 1000
|
|
33
|
+
local SEQ_KEY = "rlog:" .. domain .. ":seq"
|
|
34
|
+
local slot_key = function(seq) return "rlog:" .. domain .. ":" .. (seq % RING) end
|
|
29
35
|
|
|
30
36
|
ngx.header["Content-Type"] = "text/event-stream"
|
|
31
37
|
ngx.header["Cache-Control"] = "no-cache, no-store"
|
|
@@ -36,55 +42,62 @@ ngx.header["X-Accel-Buffering"] = "no"
|
|
|
36
42
|
-- Without this, ngx.flush on an empty buffer sends chunked-EOF (0\r\n\r\n)
|
|
37
43
|
-- and terminates the response immediately.
|
|
38
44
|
ngx.print(": connected\n\n")
|
|
39
|
-
if not ngx.flush(true) then
|
|
40
|
-
sh:delete(SUB_KEY)
|
|
41
|
-
return
|
|
42
|
-
end
|
|
45
|
+
if not ngx.flush(true) then return end
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
-- Start at the current head: stream only requests that arrive AFTER connect.
|
|
48
|
+
-- History up to this point is served separately by /logs/recent, and the client
|
|
49
|
+
-- dedups the two by the deterministic `{host}:{seq}` id, so a row seen by both
|
|
50
|
+
-- collapses to one.
|
|
51
|
+
local cursor = tonumber(sh:get(SEQ_KEY)) or 0
|
|
52
|
+
local started = ngx.now()
|
|
53
|
+
local last_hb = started
|
|
47
54
|
|
|
48
55
|
while true do
|
|
49
|
-
-- Max 1 hour per connection
|
|
50
|
-
if ngx.now() - started > 3600 then
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
-- Max 1 hour per connection.
|
|
57
|
+
if ngx.now() - started > 3600 then return end
|
|
58
|
+
|
|
59
|
+
local head = tonumber(sh:get(SEQ_KEY)) or 0
|
|
60
|
+
|
|
61
|
+
-- If we've fallen more than a full ring behind, the oldest unread slots have
|
|
62
|
+
-- already been overwritten. Jump to the oldest slot still holding its own
|
|
63
|
+
-- entry (head-RING maps to head's slot, so head-RING+1 is the oldest live one)
|
|
64
|
+
-- and skip the lost range — same bound the old 2000-entry queue had.
|
|
65
|
+
if cursor < head - RING then cursor = head - RING end
|
|
54
66
|
|
|
55
|
-
-- Drain up to 100 queued entries per cycle
|
|
56
67
|
local sent = 0
|
|
57
|
-
while sent < 100 do
|
|
58
|
-
local
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
while cursor < head and sent < 100 do
|
|
69
|
+
local next_seq = cursor + 1
|
|
70
|
+
local entry = sh:get(slot_key(next_seq))
|
|
71
|
+
if entry then
|
|
72
|
+
ngx.print("event: request\ndata: ", entry, "\n\n")
|
|
73
|
+
sent = sent + 1
|
|
74
|
+
cursor = next_seq
|
|
75
|
+
elseif next_seq < head then
|
|
76
|
+
-- A later seq already exists, so this can't be the incr(seq)→set(slot)
|
|
77
|
+
-- write window: the slot is genuinely lost (expired or wrapped). Skip it.
|
|
78
|
+
cursor = next_seq
|
|
79
|
+
else
|
|
80
|
+
-- next_seq == head: the newest entry's slot may not be written yet
|
|
81
|
+
-- (the microsecond window between incr(seq) and set(slot)). Retry it
|
|
82
|
+
-- next cycle without advancing.
|
|
83
|
+
break
|
|
84
|
+
end
|
|
62
85
|
end
|
|
63
86
|
|
|
64
87
|
if sent > 0 then
|
|
65
|
-
if not ngx.flush(true) then
|
|
66
|
-
sh:delete(SUB_KEY)
|
|
67
|
-
return
|
|
68
|
-
end
|
|
88
|
+
if not ngx.flush(true) then return end
|
|
69
89
|
end
|
|
70
90
|
|
|
71
91
|
local now = ngx.now()
|
|
72
92
|
|
|
73
|
-
-- Heartbeat every 15s
|
|
93
|
+
-- Heartbeat every 15s. Its flush is also how a dead client is detected: once
|
|
94
|
+
-- the reader (a `docker exec curl`) goes away and its socket closes, this
|
|
95
|
+
-- flush fails and the connection tears down here.
|
|
74
96
|
if now - last_hb > 15 then
|
|
75
97
|
ngx.print(": ping\n\n")
|
|
76
|
-
if not ngx.flush(true) then
|
|
77
|
-
sh:delete(SUB_KEY)
|
|
78
|
-
return
|
|
79
|
-
end
|
|
98
|
+
if not ngx.flush(true) then return end
|
|
80
99
|
last_hb = now
|
|
81
100
|
end
|
|
82
101
|
|
|
83
|
-
-- Refresh subscriber TTL every 10s
|
|
84
|
-
if now - last_ref > 10 then
|
|
85
|
-
sh:set(SUB_KEY, true, 30)
|
|
86
|
-
last_ref = now
|
|
87
|
-
end
|
|
88
|
-
|
|
89
102
|
ngx.sleep(0.05)
|
|
90
103
|
end
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
-- Shared dict key schema (request_data zone):
|
|
28
28
|
-- rlog:{domain}:seq monotonic write pointer
|
|
29
29
|
-- rlog:{domain}:{slot} JSON entry (ring buf) (TTL 1h)
|
|
30
|
-
--
|
|
31
|
-
--
|
|
30
|
+
--
|
|
31
|
+
-- The ring is ALSO the live source: pipe_stream.lua's SSE endpoint reads these
|
|
32
|
+
-- slots by a per-connection cursor, so there is no separate live queue or
|
|
33
|
+
-- subscriber flag — every watcher sees the full stream and steals from none.
|
|
32
34
|
--
|
|
33
35
|
-- NOTE on `:u` — it counts non-static REQUESTS, not people. It was surfaced as
|
|
34
36
|
-- "unique IPs" all the way to the dashboard, which it never was. Real distinct
|
|
@@ -43,10 +45,6 @@ if not geo_ok then
|
|
|
43
45
|
ngx.log(ngx.WARN, "[site_logger] geo_country module not available. GeoIP disabled.")
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
-- SAFE LOAD: Pipe module (per-worker, cached by require)
|
|
47
|
-
local pipe_ok, pipe_log = pcall(require, "openship.pipe_log")
|
|
48
|
-
if not pipe_ok then pipe_log = nil end
|
|
49
|
-
|
|
50
48
|
local analytics = ngx.shared.analytics
|
|
51
49
|
local request_data = ngx.shared.request_data
|
|
52
50
|
-- Per-host edge config, pushed by the API (see mgmt_api's /analytics/config).
|
|
@@ -194,11 +192,12 @@ local status = ngx.var.status or "0"
|
|
|
194
192
|
|
|
195
193
|
-- Client country, resolved ONCE per request.
|
|
196
194
|
--
|
|
197
|
-
-- Hoisted out of the geo block because
|
|
198
|
-
--
|
|
199
|
-
--
|
|
200
|
-
--
|
|
201
|
-
--
|
|
195
|
+
-- Hoisted out of the geo block because both the daily/per-minute rollups and the raw
|
|
196
|
+
-- ring buffer need it. The ring is the single source for the request log — /logs/recent
|
|
197
|
+
-- reads it directly and the live SSE stream (pipe_stream.lua) reads the same slots by
|
|
198
|
+
-- cursor — so resolving the country here means every row carries its flag no matter
|
|
199
|
+
-- which path delivered it. An earlier split (looked up in the rollup, again in a
|
|
200
|
+
-- separate live pipe) left the ring with no country at all.
|
|
202
201
|
--
|
|
203
202
|
-- pcall'd because a corrupt or partially-written mmdb makes the lookup raise, and an
|
|
204
203
|
-- error here would abort every counter below it (see the section header).
|
|
@@ -356,10 +355,20 @@ pcall(function()
|
|
|
356
355
|
bump_indexed(gpfx .. "s:" .. status, D48H, gpfx .. "sn", gpfx .. "si:", status)
|
|
357
356
|
end)
|
|
358
357
|
|
|
359
|
-
-- ── 4. Raw request ring buffer
|
|
358
|
+
-- ── 4. Raw request ring buffer (also the live SSE source) ────────────────────
|
|
359
|
+
--
|
|
360
|
+
-- `seq` is claimed BEFORE encoding so the stored JSON can carry a deterministic
|
|
361
|
+
-- id (`{host}:{seq}`). That id is the dedup key everywhere downstream: /logs/recent
|
|
362
|
+
-- returns these entries verbatim, and pipe_stream.lua streams the same slots live,
|
|
363
|
+
-- so the same request reaches the client with one stable id instead of a fresh
|
|
364
|
+
-- random one per source. `incr` with init 0 never returns nil here; guard anyway
|
|
365
|
+
-- so a shdict hiccup can't index a nil into the slot key.
|
|
360
366
|
|
|
361
367
|
pcall(function()
|
|
368
|
+
local seq = request_data:incr("rlog:" .. host .. ":seq", 1, 0)
|
|
369
|
+
if not seq then return end
|
|
362
370
|
local ok_j, j = pcall(cjson.encode, {
|
|
371
|
+
id = host .. ":" .. seq,
|
|
363
372
|
ip = ip,
|
|
364
373
|
-- nil is simply omitted by cjson, so a box with no GeoIP writes the same shape
|
|
365
374
|
-- minus this key rather than a null the reader has to special-case.
|
|
@@ -374,17 +383,6 @@ pcall(function()
|
|
|
374
383
|
rt = rt,
|
|
375
384
|
})
|
|
376
385
|
if ok_j and j then
|
|
377
|
-
|
|
378
|
-
local slot = seq % RING
|
|
379
|
-
request_data:set("rlog:" .. host .. ":" .. slot, j, D1H)
|
|
386
|
+
request_data:set("rlog:" .. host .. ":" .. (seq % RING), j, D1H)
|
|
380
387
|
end
|
|
381
388
|
end)
|
|
382
|
-
|
|
383
|
-
-- ── 5. Live-log pipe (only when a subscriber is watching) ───────────────────
|
|
384
|
-
-- Fire via timer to avoid blocking the log phase
|
|
385
|
-
|
|
386
|
-
if pipe_log and pipe_log.pipe_request_log
|
|
387
|
-
and request_data:get("log_pipe:sub:" .. host) then
|
|
388
|
-
ngx.timer.at(0, pipe_log.pipe_request_log,
|
|
389
|
-
host, ip, ts, ua, uri, req_len, bytes, rt, method, status, cc)
|
|
390
|
-
end
|
|
@@ -4,15 +4,15 @@ import { createRequire as __ospCreateRequire } from "node:module";
|
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
SystemManager
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-3NZICQHF.js";
|
|
8
|
+
import "./chunk-IOT5ZX23.js";
|
|
9
9
|
import "./chunk-IW2UKBVF.js";
|
|
10
10
|
import "./chunk-ZCFVHA25.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-EW6KB2J7.js";
|
|
12
12
|
import "./chunk-I3DPMTSF.js";
|
|
13
13
|
import "./chunk-VZ2ZHPS2.js";
|
|
14
14
|
import "./chunk-3LDWWY7U.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-YIPMDGG6.js";
|
|
16
16
|
import "./chunk-ZFJXPCQP.js";
|
|
17
17
|
import "./chunk-OTLSJYAC.js";
|
|
18
18
|
import "./chunk-PXKCZB2I.js";
|
package/package.json
CHANGED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
-- pipe_log.lua
|
|
2
|
-
-- Push request log into shared dict queue log_pipe:q:{host} for /stream SSE.
|
|
3
|
-
-- Payload shape matches requestLogger.js (id, ip, country when known,
|
|
4
|
-
-- timestamp, date, uri, method, status, userAgent, requestSize, responseSize,
|
|
5
|
-
-- responseTime).
|
|
6
|
-
--
|
|
7
|
-
-- Called via ngx.timer.at from site_logger (premature is first arg).
|
|
8
|
-
|
|
9
|
-
local cjson = require "cjson.safe"
|
|
10
|
-
|
|
11
|
-
local SUB_PREFIX = "log_pipe:sub:"
|
|
12
|
-
local QUEUE_PREFIX = "log_pipe:q:"
|
|
13
|
-
local QUEUE_MAX_LEN = 2000
|
|
14
|
-
|
|
15
|
-
-- SAFE LOAD: GeoIP (per-worker, cached by require)
|
|
16
|
-
local geo_ok, geo = pcall(require, "openship.geo_country")
|
|
17
|
-
if not geo_ok then geo = nil end
|
|
18
|
-
|
|
19
|
-
-- Reused payload table per worker (cleared after encode to avoid GC pressure)
|
|
20
|
-
local payload = {}
|
|
21
|
-
|
|
22
|
-
-- `cc` is the country site_logger already resolved for this request. Passed in rather
|
|
23
|
-
-- than looked up again: one mmdb hit per request, and the live frame can never disagree
|
|
24
|
-
-- with the rollup or the ring buffer about where a hit came from.
|
|
25
|
-
local function pipe_request_log(premature, host, ip, ts, ua, uri,
|
|
26
|
-
req_len, bytes, rt, method, status, cc)
|
|
27
|
-
if premature then return end
|
|
28
|
-
|
|
29
|
-
local sh = ngx.shared.request_data
|
|
30
|
-
if not sh then return end
|
|
31
|
-
|
|
32
|
-
-- Double-check subscriber is still active (guard against race)
|
|
33
|
-
if not sh:get(SUB_PREFIX .. host) then return end
|
|
34
|
-
|
|
35
|
-
pcall(function()
|
|
36
|
-
-- Falls back to its own lookup only if the caller passed nothing, which happens
|
|
37
|
-
-- when a box is mid-deploy and still has the previous site_logger.
|
|
38
|
-
local country = cc
|
|
39
|
-
if (not country or country == "") and geo and geo.get_country_code then
|
|
40
|
-
local ok_cc, res = pcall(geo.get_country_code, ip)
|
|
41
|
-
if ok_cc then country = res end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
payload.id = string.format("%.3f-%d", ts, math.random(10000, 99999))
|
|
45
|
-
payload.host = host
|
|
46
|
-
payload.ip = ip
|
|
47
|
-
if country and type(country) == "string" and country ~= "" then
|
|
48
|
-
payload.country = country
|
|
49
|
-
end
|
|
50
|
-
payload.timestamp = ts
|
|
51
|
-
payload.date = os.date("!%Y-%m-%d %H:%M:%S", ts)
|
|
52
|
-
payload.uri = uri
|
|
53
|
-
payload.method = method or "GET"
|
|
54
|
-
payload.status = tonumber(status) or 0
|
|
55
|
-
payload.userAgent = ua or ""
|
|
56
|
-
payload.requestSize = tonumber(req_len) or 0
|
|
57
|
-
payload.responseSize = tonumber(bytes) or 0
|
|
58
|
-
payload.responseTime = tonumber(rt) or 0
|
|
59
|
-
|
|
60
|
-
local body = cjson.encode(payload)
|
|
61
|
-
|
|
62
|
-
-- Clear payload table for next reuse
|
|
63
|
-
for k in pairs(payload) do payload[k] = nil end
|
|
64
|
-
|
|
65
|
-
if not body then return end
|
|
66
|
-
|
|
67
|
-
local qkey = QUEUE_PREFIX .. host
|
|
68
|
-
sh:lpush(qkey, body)
|
|
69
|
-
if sh:llen(qkey) > QUEUE_MAX_LEN then
|
|
70
|
-
sh:rpop(qkey)
|
|
71
|
-
end
|
|
72
|
-
end)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
pipe_request_log = pipe_request_log,
|
|
77
|
-
}
|