From b089e21833d9c8fb6197adc83e7b9fefa4b8a9a7 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 12 Jan 2024 17:54:12 +0800 Subject: [PATCH] add config to control how pool behaves when empty --- LICENSE | 2 +- readme.md | 8 ++++++-- src/config.zig | 10 ++++++++-- src/pool.zig | 37 +++++++++++++++++++++++++++++++++---- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/LICENSE b/LICENSE index 8ff2a00..a612ad9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -iMozilla Public License Version 2.0 +Mozilla Public License Version 2.0 ================================== 1. Definitions diff --git a/readme.md b/readme.md index aef8067..65b0ac2 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ Structured Logging for Zig -logz is an opinionated structured logger that outputs to stdout using logfmt or JSON. It aims to minimize runtime memory allocation by using a pool of pre-allocated loggers. +logz is an opinionated structured logger that outputs to stdout or stderr using logfmt or JSON. It aims to minimize runtime memory allocation by using a pool of pre-allocated loggers. # Installation This library supports native Zig module (introduced in 0.11). Add a "logz" dependency to your `build.zig.zon`. @@ -41,7 +41,7 @@ requestLogs.err(). # Important Notes 1. Attribute keys are never escaped. logz assumes that attribute keys can be written as is. 2. Logz will silently truncate attributes if the log entry exceeds the configured `max_size`. This truncation only happens at the attribute level (not in the middle of a key or value), thus either the whole key=value is written or none of it is. -3. If the pool is empty, logz will attempt to dynamically allocate a new logger. If this fails, a noop logger will be return. The log entry is silently dropped. An error message **is** written using `std.log.err`. +3. Depending on the `pool_empty` configuration, when empty the pool will either dynamically create a logger (`.pool_empty = .create`) or return a noop logger (`.pool_empty = .noop)`. If creation fails, a noop logger will be return and an error is written using `std.log.err`. When `.pool_empty = .noop`, then there will be no memory allocations made after `Pool.init` returns. ## Pools and Loggers Pools are thread-safe. @@ -120,6 +120,10 @@ pub const Config = struct { // The number of loggers to pre-allocate. pool_size: usize = 32, + // Controls what the pool does when empty. It can either dynamically create + // a new Logger, or return the Noop logger. + pool_empty: PoolEmpty = .create, + // The maximum size, in bytes, that each log entry can be. // Writing more data than max_size will truncate the log at a key=value // boundary (in other words, you won't get a key or value randomly diff --git a/src/config.zig b/src/config.zig index 21dddce..e7cbc10 100644 --- a/src/config.zig +++ b/src/config.zig @@ -7,14 +7,20 @@ pub const Config = struct { prefix: ?[]const u8 = null, output: Output = .stdout, encoding: Encoding = .logfmt, + pool_empty: PoolEmpty = .create, - const Output = enum { + pub const Output = enum { stdout, stderr, }; - const Encoding = enum { + pub const Encoding = enum { json, logfmt, }; + + pub const PoolEmpty = enum { + create, + noop, + }; }; diff --git a/src/pool.zig b/src/pool.zig index d46ef65..b2b405c 100644 --- a/src/pool.zig +++ b/src/pool.zig @@ -16,6 +16,7 @@ pub const Pool = struct { available: usize, loggers: []Logger, allocator: Allocator, + empty: Config.PoolEmpty, pub fn init(allocator: Allocator, config: Config) !*Pool { const size = config.pool_size; @@ -32,6 +33,7 @@ pub const Pool = struct { .loggers = loggers, .available = size, .allocator = allocator, + .empty = config.pool_empty, .level = @intFromEnum(config.level), }; @@ -68,6 +70,10 @@ pub const Pool = struct { // dont hold the lock over factory self.mutex.unlock(); + if (self.empty == .noop) { + return logz.noop; + } + const l = self.createLogger() catch |e| { logDynamicAllocationFailure(e); return logz.noop; @@ -268,14 +274,37 @@ test "pool: acquire and release" { try t.expectEqual(false, l1a.inner.logfmt == l2a.inner.logfmt); try t.expectEqual(false, l2a.inner.logfmt == l3a.inner.logfmt); - p.release(l1a); + l1a.release(); + + const l1b = p.acquire(); + try t.expectEqual(true, l1a.inner.logfmt == l1b.inner.logfmt); + + l3a.release(); + l2a.release(); + l1b.release(); +} + +test "pool: empty noop" { + // not 100% sure this is testing exactly what I want, but it's ....something ? + const min_config = Config{.pool_size = 2, .max_size = 1, .pool_empty = .noop}; + var p = try Pool.init(t.allocator, min_config); + defer p.deinit(); + + const l1a = p.acquire(); + const l2a = p.acquire(); + const l3a = p.acquire(); // this should be noop + + try t.expectEqual(false, l1a.inner.logfmt == l2a.inner.logfmt); + try t.expectEqual(.noop, std.meta.activeTag(l3a.inner)); + + l1a.release(); const l1b = p.acquire(); try t.expectEqual(true, l1a.inner.logfmt == l1b.inner.logfmt); - p.release(l3a); - p.release(l2a); - p.release(l1b); + l3a.release(); + l2a.release(); + l1b.release(); } test "pool: log to kv" {