Compare commits
2 commits
56244024ff
...
297c1c8bb8
| Author | SHA1 | Date | |
|---|---|---|---|
| 297c1c8bb8 | |||
| b422718f3a |
2 changed files with 92 additions and 13 deletions
|
|
@ -14,8 +14,8 @@
|
||||||
.hash = "httpz-0.0.0-PNVzrLjJCAD37S0CcrXpsjSqr86hVjK0rsALTDJ98AAJ",
|
.hash = "httpz-0.0.0-PNVzrLjJCAD37S0CcrXpsjSqr86hVjK0rsALTDJ98AAJ",
|
||||||
},
|
},
|
||||||
.zfin = .{
|
.zfin = .{
|
||||||
.url = "git+https://git.lerch.org/lobo/zfin#641a88b0b768769a33e3f95fa6f1736fa58e4a6f",
|
.url = "git+https://git.lerch.org/lobo/zfin#4d65cc45f4fd7c71a4222d2d3c7c41100acd7069",
|
||||||
.hash = "zfin-0.0.0-J-B21mEEPACavg9MPzB402B3DIzfIxXhNv3UOAyFHBxK",
|
.hash = "zfin-0.0.0-J-B21ls4PADNXleWi_cuaAYNjBWEO0FfO3qyBnCPtE3R",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
101
src/main.zig
101
src/main.zig
|
|
@ -22,16 +22,26 @@ const App = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
config: zfin.Config,
|
config: zfin.Config,
|
||||||
svc: zfin.DataService,
|
svc: zfin.DataService,
|
||||||
|
/// Threshold in milliseconds above which a request is logged
|
||||||
|
/// as slow. Tunable via `ZFIN_SERVER_SLOW_MS` env var; defaults
|
||||||
|
/// to 500ms. Captured once at App.init so the dispatch hot path
|
||||||
|
/// doesn't re-parse on every request.
|
||||||
|
slow_threshold_ms: u64,
|
||||||
|
|
||||||
fn init(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) App {
|
fn init(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) App {
|
||||||
const config = zfin.Config.fromEnv(io, allocator, environ);
|
const config = zfin.Config.fromEnv(io, allocator, environ);
|
||||||
const svc = zfin.DataService.init(io, allocator, config);
|
const svc = zfin.DataService.init(io, allocator, config);
|
||||||
|
const slow_threshold_ms = if (environ.get("ZFIN_SERVER_SLOW_MS")) |s|
|
||||||
|
std.fmt.parseInt(u64, s, 10) catch 500
|
||||||
|
else
|
||||||
|
500;
|
||||||
return .{
|
return .{
|
||||||
.io = io,
|
.io = io,
|
||||||
.environ = environ,
|
.environ = environ,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.config = config,
|
.config = config,
|
||||||
.svc = svc,
|
.svc = svc,
|
||||||
|
.slow_threshold_ms = slow_threshold_ms,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,8 +49,46 @@ const App = struct {
|
||||||
self.svc.deinit();
|
self.svc.deinit();
|
||||||
self.config.deinit();
|
self.config.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// httpz dispatch hook: every request flows through here so we
|
||||||
|
/// have a single place to wrap timing and error logging without
|
||||||
|
/// modifying every handler. Slow requests (above
|
||||||
|
/// `slow_threshold_ms`) and error responses (status >= 400)
|
||||||
|
/// emit a structured stderr line; everything else stays silent.
|
||||||
|
pub fn dispatch(self: *App, action: httpz.Action(*App), req: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
// wall-clock required: per-request elapsed for slow-request
|
||||||
|
// logging. `.awake` (monotonic) avoids spurious negatives
|
||||||
|
// on system clock skew.
|
||||||
|
const start_ns = std.Io.Timestamp.now(self.io, .awake).nanoseconds;
|
||||||
|
try action(self, req, res);
|
||||||
|
|
||||||
|
// using defer here so we execute unconditionally
|
||||||
|
defer {
|
||||||
|
const elapsed_ns = std.Io.Timestamp.now(self.io, .awake).nanoseconds - start_ns;
|
||||||
|
const elapsed_ms: u64 = @intCast(@divTrunc(elapsed_ns, std.time.ns_per_ms));
|
||||||
|
if (shouldLogRequest(elapsed_ms, res.status, self.slow_threshold_ms)) {
|
||||||
|
// wall-clock required: ts in stderr line lets the
|
||||||
|
// operator correlate slow requests with cron / system
|
||||||
|
// events using `date -d @<ts>`.
|
||||||
|
const ts = std.Io.Timestamp.now(self.io, .real).toSeconds();
|
||||||
|
log.warn("ts={d} elapsed_ms={d} status={d} method={s} path={s}", .{
|
||||||
|
ts,
|
||||||
|
elapsed_ms,
|
||||||
|
res.status,
|
||||||
|
@tagName(req.method),
|
||||||
|
req.url.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Pure predicate: should this request emit a stderr log line?
|
||||||
|
/// Logs slow successes (above threshold) and any error response.
|
||||||
|
fn shouldLogRequest(elapsed_ms: u64, status: u16, threshold_ms: u64) bool {
|
||||||
|
return elapsed_ms > threshold_ms or status >= 400;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Route handlers ───────────────────────────────────────────
|
// ── Route handlers ───────────────────────────────────────────
|
||||||
|
|
||||||
fn handleIndex(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
fn handleIndex(_: *App, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
|
@ -416,8 +464,8 @@ fn upperDupe(allocator: std.mem.Allocator, s: []const u8) ![]u8 {
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printRateLimitWait(svc: *zfin.DataService, stdout: *std.Io.Writer) !void {
|
fn printRateLimitWait(svc: *zfin.DataService, data_type: zfin.cache.DataType, stdout: *std.Io.Writer) !void {
|
||||||
if (svc.estimateWaitSeconds()) |wait| {
|
if (svc.estimateWaitSeconds(data_type)) |wait| {
|
||||||
if (wait > 0) {
|
if (wait > 0) {
|
||||||
try stdout.print("\n (rate limit -- waiting {d}s)\n ", .{wait});
|
try stdout.print("\n (rate limit -- waiting {d}s)\n ", .{wait});
|
||||||
try stdout.flush();
|
try stdout.flush();
|
||||||
|
|
@ -558,7 +606,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
// handlers to serve. Per-symbol `getEtfMetrics` calls below
|
// handlers to serve. Per-symbol `getEtfMetrics` calls below
|
||||||
// also rely on these maps being loaded.
|
// also rely on these maps being loaded.
|
||||||
{
|
{
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .tickers_funds, stdout);
|
||||||
if (svc.loadMutualFundTickerMap(.{})) |mut_map| {
|
if (svc.loadMutualFundTickerMap(.{})) |mut_map| {
|
||||||
var m = mut_map;
|
var m = mut_map;
|
||||||
m.deinit();
|
m.deinit();
|
||||||
|
|
@ -566,7 +614,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
} else |err| {
|
} else |err| {
|
||||||
try stdout.print("EDGAR mutual-fund ticker map FAILED ({t})\n", .{err});
|
try stdout.print("EDGAR mutual-fund ticker map FAILED ({t})\n", .{err});
|
||||||
}
|
}
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .tickers_companies, stdout);
|
||||||
if (svc.loadCompanyTickerMap(.{})) |co_map| {
|
if (svc.loadCompanyTickerMap(.{})) |co_map| {
|
||||||
var m = co_map;
|
var m = co_map;
|
||||||
m.deinit();
|
m.deinit();
|
||||||
|
|
@ -586,7 +634,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
var sym_ok = true;
|
var sym_ok = true;
|
||||||
|
|
||||||
// Candles
|
// Candles
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .candles_daily, stdout);
|
||||||
if (svc.getCandles(sym, .{})) |result| {
|
if (svc.getCandles(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print("candles ok ({s})", .{@tagName(result.source)});
|
try stdout.print("candles ok ({s})", .{@tagName(result.source)});
|
||||||
|
|
@ -604,7 +652,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dividends
|
// Dividends
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .dividends, stdout);
|
||||||
if (svc.getDividends(sym, .{})) |result| {
|
if (svc.getDividends(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print(", dividends ok ({s})", .{@tagName(result.source)});
|
try stdout.print(", dividends ok ({s})", .{@tagName(result.source)});
|
||||||
|
|
@ -614,7 +662,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits
|
// Splits
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .splits, stdout);
|
||||||
if (svc.getSplits(sym, .{})) |result| {
|
if (svc.getSplits(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print(", splits ok ({s})", .{@tagName(result.source)});
|
try stdout.print(", splits ok ({s})", .{@tagName(result.source)});
|
||||||
|
|
@ -624,7 +672,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
}
|
}
|
||||||
|
|
||||||
// Earnings
|
// Earnings
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .earnings, stdout);
|
||||||
if (svc.getEarnings(sym, .{})) |result| {
|
if (svc.getEarnings(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print(", earnings ok ({s})", .{@tagName(result.source)});
|
try stdout.print(", earnings ok ({s})", .{@tagName(result.source)});
|
||||||
|
|
@ -639,7 +687,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
// Wikidata entry) and doesn't flip sym_ok.
|
// Wikidata entry) and doesn't flip sym_ok.
|
||||||
var cik_buf: ?[]u8 = null;
|
var cik_buf: ?[]u8 = null;
|
||||||
defer if (cik_buf) |b| allocator.free(b);
|
defer if (cik_buf) |b| allocator.free(b);
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .classification, stdout);
|
||||||
if (svc.getClassification(sym, .{})) |result| {
|
if (svc.getClassification(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
if (result.data.len > 0) {
|
if (result.data.len > 0) {
|
||||||
|
|
@ -660,7 +708,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
// non-funds (NPORT-P only exists for funds + UITs); a
|
// non-funds (NPORT-P only exists for funds + UITs); a
|
||||||
// negative-cache entry suppresses retries. Logged as
|
// negative-cache entry suppresses retries. Logged as
|
||||||
// `n/a` and doesn't flip sym_ok.
|
// `n/a` and doesn't flip sym_ok.
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .etf_metrics, stdout);
|
||||||
if (svc.getEtfMetrics(sym, .{})) |result| {
|
if (svc.getEtfMetrics(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print(", etf_metrics ok ({s})", .{@tagName(result.source)});
|
try stdout.print(", etf_metrics ok ({s})", .{@tagName(result.source)});
|
||||||
|
|
@ -679,7 +727,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
|
||||||
// zfin chains entity_facts off Wikidata's CIK, so the
|
// zfin chains entity_facts off Wikidata's CIK, so the
|
||||||
// server warms the cache the same way).
|
// server warms the cache the same way).
|
||||||
if (cik_buf) |cik| {
|
if (cik_buf) |cik| {
|
||||||
try printRateLimitWait(&svc, stdout);
|
try printRateLimitWait(&svc, .entity_facts, stdout);
|
||||||
if (svc.getEntityFacts(cik, .{})) |result| {
|
if (svc.getEntityFacts(cik, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print(", entity_facts ok ({s})", .{@tagName(result.source)});
|
try stdout.print(", entity_facts ok ({s})", .{@tagName(result.source)});
|
||||||
|
|
@ -814,3 +862,34 @@ test "upperDupe" {
|
||||||
defer std.testing.allocator.free(result);
|
defer std.testing.allocator.free(result);
|
||||||
try std.testing.expectEqualStrings("AAPL", result);
|
try std.testing.expectEqualStrings("AAPL", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "shouldLogRequest: fast 2xx is silent" {
|
||||||
|
try std.testing.expect(!shouldLogRequest(10, 200, 500));
|
||||||
|
try std.testing.expect(!shouldLogRequest(499, 200, 500));
|
||||||
|
try std.testing.expect(!shouldLogRequest(0, 204, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldLogRequest: slow 2xx logs" {
|
||||||
|
try std.testing.expect(shouldLogRequest(501, 200, 500));
|
||||||
|
try std.testing.expect(shouldLogRequest(2000, 200, 500));
|
||||||
|
// Boundary: == threshold is NOT logged (strict >).
|
||||||
|
try std.testing.expect(!shouldLogRequest(500, 200, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldLogRequest: any error response logs regardless of timing" {
|
||||||
|
try std.testing.expect(shouldLogRequest(1, 400, 500));
|
||||||
|
try std.testing.expect(shouldLogRequest(1, 404, 500));
|
||||||
|
try std.testing.expect(shouldLogRequest(1, 500, 500));
|
||||||
|
try std.testing.expect(shouldLogRequest(1, 503, 500));
|
||||||
|
// 3xx is not flagged as error.
|
||||||
|
try std.testing.expect(!shouldLogRequest(1, 301, 500));
|
||||||
|
try std.testing.expect(!shouldLogRequest(1, 304, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldLogRequest: custom threshold respected" {
|
||||||
|
try std.testing.expect(!shouldLogRequest(50, 200, 100));
|
||||||
|
try std.testing.expect(shouldLogRequest(150, 200, 100));
|
||||||
|
// Higher threshold (e.g. user sets ZFIN_SERVER_SLOW_MS=2000).
|
||||||
|
try std.testing.expect(!shouldLogRequest(1500, 200, 2000));
|
||||||
|
try std.testing.expect(shouldLogRequest(2500, 200, 2000));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue