From b183aec83c2d467706583ad9ea0e4e9c56077c55 Mon Sep 17 00:00:00 2001
From: Nikolai K <45662151+nikonhub@users.noreply.github.com>
Date: Tue, 12 Nov 2024 00:42:50 +0100
Subject: [PATCH] httpcaddyfile: Implement log `sampling` config (#6682)

* Allow log sampling configuration from Caddyfile

* Add log sampling adapt tests
---
 caddyconfig/httpcaddyfile/builtins.go         | 44 ++++++++++++++++++
 caddyconfig/httpcaddyfile/builtins_test.go    | 14 ++++++
 .../global_options_log_sampling.caddyfiletest | 23 ++++++++++
 .../log_sampling.caddyfiletest                | 45 +++++++++++++++++++
 4 files changed, 126 insertions(+)
 create mode 100644 caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest
 create mode 100644 caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest

diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index 165c66b25bc..eca6a2d6491 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -981,6 +981,50 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
 			}
 			cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
 
+		case "sampling":
+			d := h.Dispenser.NewFromNextSegment()
+			for d.NextArg() {
+				// consume any tokens on the same line, if any.
+			}
+
+			sampling := &caddy.LogSampling{}
+			for nesting := d.Nesting(); d.NextBlock(nesting); {
+				subdir := d.Val()
+				switch subdir {
+				case "interval":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+					interval, err := time.ParseDuration(d.Val() + "ns")
+					if err != nil {
+						return nil, d.Errf("failed to parse interval: %v", err)
+					}
+					sampling.Interval = interval
+				case "first":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+					first, err := strconv.Atoi(d.Val())
+					if err != nil {
+						return nil, d.Errf("failed to parse first: %v", err)
+					}
+					sampling.First = first
+				case "thereafter":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+					thereafter, err := strconv.Atoi(d.Val())
+					if err != nil {
+						return nil, d.Errf("failed to parse thereafter: %v", err)
+					}
+					sampling.Thereafter = thereafter
+				default:
+					return nil, d.Errf("unrecognized subdirective: %s", subdir)
+				}
+			}
+
+			cl.Sampling = sampling
+
 		case "core":
 			if !h.NextArg() {
 				return nil, h.ArgErr()
diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go
index cf746348487..c23531f22e6 100644
--- a/caddyconfig/httpcaddyfile/builtins_test.go
+++ b/caddyconfig/httpcaddyfile/builtins_test.go
@@ -62,6 +62,20 @@ func TestLogDirectiveSyntax(t *testing.T) {
 			output:      `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
 			expectError: false,
 		},
+		{
+			input: `:8080 {
+				log {
+					sampling {
+						interval 2
+						first 3
+						thereafter 4
+					}
+				}
+			}
+			`,
+			output:      `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
+			expectError: false,
+		},
 	} {
 
 		adapter := caddyfile.Adapter{
diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest
new file mode 100644
index 00000000000..12b73b2b7a4
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest
@@ -0,0 +1,23 @@
+{
+	log {
+		sampling {
+			interval 300
+			first 50
+			thereafter 40
+		}
+	}
+}
+----------
+{
+	"logging": {
+		"logs": {
+			"default": {
+				"sampling": {
+					"interval": 300,
+					"first": 50,
+					"thereafter": 40
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest
new file mode 100644
index 00000000000..b5862257235
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest
@@ -0,0 +1,45 @@
+:80 {
+	log {
+		sampling {
+			interval 300
+			first 50
+			thereafter 40
+		}
+	}
+}
+----------
+{
+	"logging": {
+		"logs": {
+			"default": {
+				"exclude": [
+					"http.log.access.log0"
+				]
+			},
+			"log0": {
+				"sampling": {
+					"interval": 300,
+					"first": 50,
+					"thereafter": 40
+				},
+				"include": [
+					"http.log.access.log0"
+				]
+			}
+		}
+	},
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":80"
+					],
+					"logs": {
+						"default_logger_name": "log0"
+					}
+				}
+			}
+		}
+	}
+}
\ No newline at end of file