|
1 | 1 | package config |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "fmt" |
| 4 | + "io" |
5 | 5 | "os" |
6 | 6 | "path/filepath" |
7 | 7 | "strings" |
8 | 8 | "time" |
9 | 9 |
|
10 | 10 | "github.com/prequel-dev/preq/internal/pkg/resolve" |
| 11 | + "github.com/prequel-dev/prequel-logmatch/pkg/timez" |
11 | 12 | "github.com/rs/zerolog/log" |
12 | 13 | "gopkg.in/yaml.v3" |
13 | 14 | ) |
14 | 15 |
|
15 | | -var ( |
16 | | - defaultConfig = `timestamps: |
17 | | -
|
18 | | - # Example: {"level":"error","error":"context deadline exceeded","time":1744570895480541,"caller":"server.go:462"} |
19 | | - - format: epochany |
20 | | - pattern: | |
21 | | - "time":(\d{16,19}) |
22 | | -
|
23 | | - # Example: 2006-01-02T15:04:05Z07:00 <log message> |
24 | | - - format: rfc3339 |
25 | | - pattern: | |
26 | | - ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})) |
27 | | -
|
28 | | - # Example: 2006/01/02 03:04:05 <log message> |
29 | | - - format: "2006/01/02 03:04:05" |
30 | | - pattern: | |
31 | | - ^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) |
32 | | -
|
33 | | - # Example: 2006-01-02 15:04:05.000 <log message> |
34 | | - # Source: ISO 8601 |
35 | | - - format: "2006-01-02 15:04:05.000" |
36 | | - pattern: | |
37 | | - ^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) |
38 | | -
|
39 | | - # Example: Apr 30 23:36:47.715984 WRN <log message> |
40 | | - # Source: RFC 3164 extended |
41 | | - - format: "Jan 2 15:04:05.000000" |
42 | | - pattern: | |
43 | | - ^([A-Z][a-z]{2}\s{1,2}\d{1,2}\s\d{2}:\d{2}:\d{2}\.\d{6}) |
44 | | -
|
45 | | - # Example: Jan 2 15:04:05 <log message> |
46 | | - # Source: RFC 3164 |
47 | | - - format: "Jan 2 15:04:05" |
48 | | - pattern: | |
49 | | - ^([A-Z][a-z]{2}\s{1,2}\d{1,2}\s\d{2}:\d{2}:\d{2}) |
50 | | -
|
51 | | - # Example: 2006-01-02 15:04:05 <log message> |
52 | | - # Source: w3c, Postgres |
53 | | - - format: "2006-01-02 15:04:05" |
54 | | - pattern: | |
55 | | - ^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) |
56 | | -
|
57 | | - # Example: I0102 15:04:05.000000 <log message> |
58 | | - # Source: go/klog |
59 | | - - format: "0102 15:04:05.000000" |
60 | | - pattern: | |
61 | | - ^[IWEF](\d{4} \d{2}:\d{2}:\d{2}\.\d{6}) |
62 | | -
|
63 | | - # Example: [2006-01-02 15:04:05,000] <log message> |
64 | | - - format: "2006-01-02 15:04:05,000" |
65 | | - pattern: | |
66 | | - ^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\] |
67 | | -
|
68 | | - # Example: 2006-01-02 15:04:05.000000-0700 <log message> |
69 | | - - format: "2006-01-02 15:04:05.000000-0700" |
70 | | - pattern: | |
71 | | - ^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}[+-]\d{4}) |
72 | | -
|
73 | | - # Example: 2006/01/02 15:04:05 <log message> |
74 | | - - format: "2006/01/02 15:04:05" |
75 | | - pattern: | |
76 | | - ^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) |
77 | | -
|
78 | | - # Example: 01/02/2006, 15:04:05 <log message> |
79 | | - # Source: IIS format |
80 | | - - format: "01/02/2006, 15:04:05" |
81 | | - pattern: | |
82 | | - ^(\d{2}/\d{2}/\d{4}, \d{2}:\d{2}:\d{2}) |
83 | | - |
84 | | - # Example: 02 Jan 2006 15:04:05.000 <log message> |
85 | | - - format: "02 Jan 2006 15:04:05.000" |
86 | | - pattern: | |
87 | | - ^(\d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2}\.\d{3}) |
88 | | - |
89 | | - # Example: 2006 Jan 02 15:04:05.000 <log message> |
90 | | - - format: "2006 Jan 02 15:04:05.000" |
91 | | - pattern: | |
92 | | - ^(\d{4} [A-Z][a-z]{2} \d{2} \d{2}:\d{2}:\d{2}\.\d{3}) |
93 | | -
|
94 | | - # Example: 02/Jan/2006:15:04:05.000 <log message> |
95 | | - - format: "02/Jan/2006:15:04:05.000" |
96 | | - pattern: | |
97 | | - ^(\d{2}/[A-Z][a-z]{2}/\d{4}:\d{2}:\d{2}:\d{2}\.\d{3}) |
98 | | -
|
99 | | - # Example: 01/02/2006 03:04:05 PM <log message> |
100 | | - - format: "01/02/2006 03:04:05 PM" |
101 | | - pattern: | |
102 | | - ^(\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2} (AM|PM)) |
103 | | -
|
104 | | - # Example: 2006 Jan 02 15:04:05 <log message> |
105 | | - - format: "2006 Jan 02 15:04:05" |
106 | | - pattern: | |
107 | | - ^(\d{4} [A-Z][a-z]{2} \d{2} \d{2}:\d{2}:\d{2}) |
108 | | -
|
109 | | - # Example: 2006-01-02 15:04:05.000 <log message> |
110 | | - - format: "2006-01-02 15:04:05.000" |
111 | | - pattern: | |
112 | | - ^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) |
113 | | -
|
114 | | - # Example: {"timestamp":"2025-03-26T14:01:02Z","level":"info", "message":"..."} |
115 | | - # Source: Postgres JSON output |
116 | | - - format: rfc3339 |
117 | | - pattern: | |
118 | | - "timestamp"\s*:\s*"([^"]+)" |
119 | | - |
120 | | - # Example: {"ts":"2025-03-26T14:01:02Z","level":"info", "message":"..."} |
121 | | - # Source: metallb |
122 | | - - format: rfc3339 |
123 | | - pattern: | |
124 | | - "ts"\s*:\s*"([^"]+)" |
125 | | -
|
126 | | - # Example: [7] 2025/04/25 02:01:04.339092 [ERR] 10.0.6.53:27827 - cid:10110160 - TLS handshake error: EOF |
127 | | - # Source: NATS |
128 | | - - format: "2006/01/02 15:04:05.000000" |
129 | | - pattern: | |
130 | | - ^\[\d+\]\s+(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) |
131 | | -
|
132 | | - # Example: {"creationTimestamp":"2025-04-23T20:50:35Z","name":"insecure-nginx-conf","namespace":"default","resourceVersion":"825013"} |
133 | | - # Source: Kubernetes events, configmaps |
134 | | - - format: rfc3339 |
135 | | - pattern: | |
136 | | - "creationTimestamp":"([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)" |
137 | | -
|
138 | | - # Example: 2025-04-24T21:55:08.535-0500 INFO example-log-entry |
139 | | - # Source: ZAP production |
140 | | - - format: "2006-01-02T15:04:05.000-0700" |
141 | | - pattern: | |
142 | | - ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4}) |
143 | | -
|
144 | | - # Example: {"level":"info","ts":1745549708.5355184,"msg":"example-log-entry"} |
145 | | - # Source: ZAP development |
146 | | - - format: epochany |
147 | | - pattern: | |
148 | | - "ts"\s*:\s*([0-9]+)(?:\.[0-9]+)? |
149 | | -
|
150 | | - # Example: ts=2025-03-10T13:52:40.623431174Z level=info msg="tail routine: tail channel closed... |
151 | | - # Source: Loki |
152 | | - - format: "2006-01-02T15:04:05.000000000Z" |
153 | | - pattern: | |
154 | | - ts=([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9}Z) |
155 | | -
|
156 | | - # Example: {"event": "DD_API_KEY undefined. Metrics, logs and events will not be reported to DataDog", "timestamp": "2025-02-12T18:12:58.715528Z", "level": "warn... |
157 | | - # Source: DataDog |
158 | | - - format: "2006-01-02T15:04:05.000000Z" |
159 | | - pattern: | |
160 | | - "timestamp"\s*:\s*"([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z)" |
161 | | -
|
162 | | - # Example: {"Id":19,"Version":1,"Opcode":13,"RecordId":1493,"LogName":"System","ProcessId":4324,"ThreadId":10456,"MachineName":"windows","TimeCreated":"\/Date(1743448267142)\/"} |
163 | | - # Source; Windows events via Get-Events w/ JSON output |
164 | | - - format: epochany |
165 | | - pattern: | |
166 | | - /Date\((\d+)\) |
167 | | -
|
168 | | - # Example: time="2025-02-12T18:12:58.715528Z" |
169 | | - # Source: argocd |
170 | | - - format: rfc3339 |
171 | | - pattern: | |
172 | | - time="([^"]+)" |
173 | | -` |
174 | | - |
175 | | - windowConfig = `window: %s` |
176 | | -) |
177 | | - |
178 | 16 | type Config struct { |
179 | 17 | TimestampRegexes []Regex `yaml:"timestamps"` |
180 | 18 | Rules Rules `yaml:"rules"` |
@@ -216,57 +54,60 @@ func parseOpts(opts ...OptT) *optsT { |
216 | 54 | return o |
217 | 55 | } |
218 | 56 |
|
219 | | -func Marshal(opts ...OptT) string { |
220 | | - o := parseOpts(opts...) |
221 | | - |
222 | | - if o.window > 0 { |
223 | | - wcfg := fmt.Sprintf(windowConfig, o.window) |
224 | | - return fmt.Sprintf("%s\n%s\n", defaultConfig, wcfg) |
225 | | - } |
226 | | - |
227 | | - return defaultConfig |
228 | | -} |
| 57 | +// LoadConfig loads the configuration from the specified directory and file. |
| 58 | +// If the file does not exist, it returns a default configuration. |
229 | 59 |
|
230 | 60 | func LoadConfig(dir, file string, opts ...OptT) (*Config, error) { |
231 | 61 |
|
232 | | - var config Config |
233 | | - |
234 | | - if _, err := os.Stat(dir); os.IsNotExist(err) { |
235 | | - if err := os.MkdirAll(dir, 0755); err != nil { |
236 | | - return nil, err |
237 | | - } |
| 62 | + spec := filepath.Join(dir, file) |
| 63 | + _, err := os.Stat(spec) |
| 64 | + |
| 65 | + switch { |
| 66 | + case err == nil: // NOOP |
| 67 | + case os.IsNotExist(err): |
| 68 | + log.Info(). |
| 69 | + Str("file", spec). |
| 70 | + Msg("Configuration file does not exist, using default configuration") |
| 71 | + return DefaultConfig(opts...), nil |
| 72 | + default: |
| 73 | + return nil, err |
238 | 74 | } |
239 | 75 |
|
240 | | - if _, err := os.Stat(filepath.Join(dir, file)); os.IsNotExist(err) { |
241 | | - if err := WriteDefaultConfig(filepath.Join(dir, file), opts...); err != nil { |
242 | | - log.Error().Err(err).Msg("Failed to write default config") |
243 | | - return nil, err |
244 | | - } |
| 76 | + log.Info().Str("file", spec).Msg("Loading configuration file") |
| 77 | + fh, err := os.OpenFile(spec, os.O_RDONLY, 0644) |
| 78 | + if err != nil { |
| 79 | + return nil, err |
245 | 80 | } |
| 81 | + defer fh.Close() |
| 82 | + |
| 83 | + return ReadConfig(fh) |
| 84 | +} |
246 | 85 |
|
247 | | - data, err := os.ReadFile(filepath.Join(dir, file)) |
| 86 | +func ReadConfig(rd io.Reader) (*Config, error) { |
| 87 | + data, err := io.ReadAll(rd) |
248 | 88 | if err != nil { |
249 | 89 | return nil, err |
250 | 90 | } |
251 | | - |
| 91 | + var config Config |
252 | 92 | if err := yaml.Unmarshal(data, &config); err != nil { |
253 | 93 | return nil, err |
254 | 94 | } |
255 | | - |
256 | 95 | return &config, nil |
257 | 96 | } |
258 | 97 |
|
259 | | -func WriteDefaultConfig(path string, opts ...OptT) error { |
260 | | - cfg := Marshal(opts...) |
261 | | - return os.WriteFile(path, []byte(cfg), 0644) |
262 | | -} |
| 98 | +func DefaultConfig(opts ...OptT) *Config { |
| 99 | + o := parseOpts(opts...) |
263 | 100 |
|
264 | | -func LoadConfigFromBytes(data string) (*Config, error) { |
265 | | - var config Config |
266 | | - if err := yaml.Unmarshal([]byte(data), &config); err != nil { |
267 | | - return nil, err |
| 101 | + c := &Config{ |
| 102 | + Window: o.window, |
268 | 103 | } |
269 | | - return &config, nil |
| 104 | + for _, r := range timez.Defaults { |
| 105 | + c.TimestampRegexes = append(c.TimestampRegexes, Regex{ |
| 106 | + Pattern: r.Pattern, |
| 107 | + Format: string(r.Format), |
| 108 | + }) |
| 109 | + } |
| 110 | + return c |
270 | 111 | } |
271 | 112 |
|
272 | 113 | func (c *Config) ResolveOpts() (opts []resolve.OptT) { |
|
0 commit comments