ez_logs_agent 0.1.0

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.
data/CONFIGURATION.md ADDED
@@ -0,0 +1,752 @@
1
+ # EZLogs Agent — Configuration Reference
2
+
3
+ Complete reference for all configuration options.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Required Settings](#required-settings)
10
+ - [Event Capture Toggles](#event-capture-toggles)
11
+ - [Exclusion Lists](#exclusion-lists)
12
+ - [Display Names](#display-names)
13
+ - [Actor Context](#actor-context)
14
+ - [Transport Settings](#transport-settings)
15
+ - [Logging](#logging)
16
+ - [Environment Variables](#environment-variables)
17
+ - [Validation](#validation)
18
+
19
+ ---
20
+
21
+ ## Required Settings
22
+
23
+ ### `server_url`
24
+
25
+ **Type:** String
26
+ **Required:** Yes
27
+ **Default:** None
28
+
29
+ The URL of your EZLogs server where events will be sent.
30
+
31
+ **Example:**
32
+ ```ruby
33
+ config.server_url = "https://your-ezlogs-server.com"
34
+ ```
35
+
36
+ **Validation:**
37
+ - Must be present
38
+ - Must start with `http://` or `https://`
39
+ - Must be a valid URL format
40
+
41
+ **Common values:**
42
+ - Production: `https://your-ezlogs-server.com`
43
+ - Staging: `https://staging.your-ezlogs-server.com`
44
+ - Development: `http://localhost:3000`
45
+
46
+ ---
47
+
48
+ ### `project_token`
49
+
50
+ **Type:** String
51
+ **Required:** Yes (for authentication)
52
+ **Default:** None
53
+
54
+ Your API key from the EZLogs dashboard. This is sent as a Bearer token in the `Authorization` header.
55
+
56
+ **Example:**
57
+ ```ruby
58
+ config.project_token = "ezl_abc123xyz..."
59
+ ```
60
+
61
+ **Best practice:** Use environment variables
62
+
63
+ ```ruby
64
+ config.project_token = ENV['EZLOGS_API_KEY']
65
+ ```
66
+
67
+ **Validation:**
68
+ - Should be present (warning if missing)
69
+ - Never logged or exposed in error messages
70
+ - Sent over HTTPS only
71
+
72
+ **Where to get it:**
73
+ 1. Log into your EZLogs dashboard
74
+ 2. Go to Settings → API Keys
75
+ 3. Create a new key or copy an existing one
76
+ 4. Keys start with `ezl_` prefix
77
+
78
+ ---
79
+
80
+ ## Event Capture Toggles
81
+
82
+ ### `capture_http`
83
+
84
+ **Type:** Boolean
85
+ **Required:** No
86
+ **Default:** `true`
87
+
88
+ Enable or disable HTTP request capture.
89
+
90
+ **Example:**
91
+ ```ruby
92
+ config.capture_http = true
93
+ ```
94
+
95
+ **When enabled, captures:**
96
+ - HTTP method, path, status code, duration
97
+ - Controller and action name (Rails apps)
98
+ - GraphQL operation name and type (queries, mutations, subscriptions)
99
+ - Request correlation ID
100
+
101
+ **When to disable:**
102
+ - API-only apps where HTTP requests aren't meaningful
103
+ - Reducing noise in write-heavy applications
104
+
105
+ ---
106
+
107
+ ### `capture_jobs`
108
+
109
+ **Type:** Boolean
110
+ **Required:** No
111
+ **Default:** `true`
112
+
113
+ Enable or disable background job capture.
114
+
115
+ **Example:**
116
+ ```ruby
117
+ config.capture_jobs = true
118
+ ```
119
+
120
+ **When enabled, captures:**
121
+ - Sidekiq job executions
122
+ - ActiveJob executions (any backend)
123
+ - Job class, queue, duration
124
+ - Success/failure status with error messages
125
+
126
+ **When to disable:**
127
+ - Apps without background jobs
128
+ - Reducing noise from high-frequency jobs
129
+
130
+ ---
131
+
132
+ ### `capture_database`
133
+
134
+ **Type:** Boolean
135
+ **Required:** No
136
+ **Default:** `true`
137
+
138
+ Enable or disable database change capture.
139
+
140
+ **Example:**
141
+ ```ruby
142
+ config.capture_database = true
143
+ ```
144
+
145
+ **When enabled, captures:**
146
+ - ActiveRecord create, update, destroy operations
147
+ - Model class, record ID, operation type
148
+ - For updates: meaningful attribute changes
149
+
150
+ **When to disable:**
151
+ - Write-heavy applications with excessive database activity
152
+ - Apps where database changes aren't meaningful to business logic
153
+
154
+ ---
155
+
156
+ ## Exclusion Lists
157
+
158
+ All exclusion lists are **additive** — they add to built-in defaults, not replace them.
159
+
160
+ ### `excluded_paths`
161
+
162
+ **Type:** Array of Strings
163
+ **Required:** No
164
+ **Default:** `[]` (uses built-in defaults only)
165
+
166
+ Additional HTTP paths to exclude from capture.
167
+
168
+ **Example:**
169
+ ```ruby
170
+ config.excluded_paths = ["/admin*", "/internal*", "/api/internal*"]
171
+ ```
172
+
173
+ **Pattern matching:**
174
+ - Use `*` suffix for prefix matching
175
+ - `/admin*` matches `/admin`, `/admin/users`, `/admin/anything`
176
+ - Exact matches without `*` are supported but rarely needed
177
+
178
+ **Built-in exclusions (automatically excluded):**
179
+ - `/rails/active_storage*` — File uploads/downloads
180
+ - `/assets*`, `/packs*`, `/vite*` — Static assets
181
+ - `/health*`, `/up` — Health check endpoints
182
+ - `/favicon.ico` — Browser requests
183
+
184
+ **Common additions:**
185
+ - `/admin*` — Admin panel requests
186
+ - `/internal*` — Internal API endpoints
187
+ - `/debug*` — Debugging tools
188
+
189
+ ---
190
+
191
+ ### `excluded_tables`
192
+
193
+ **Type:** Array of Strings
194
+ **Required:** No
195
+ **Default:** `[]` (uses built-in defaults only)
196
+
197
+ Additional database tables to exclude from capture.
198
+
199
+ **Example:**
200
+ ```ruby
201
+ config.excluded_tables = ["audit_logs", "versions", "paper_trail_versions"]
202
+ ```
203
+
204
+ **Table names:**
205
+ - Use exact table name (lowercase, plural)
206
+ - No prefix matching — must match exactly
207
+
208
+ **Built-in exclusions (automatically excluded):**
209
+ - `sessions` — Session store updates
210
+ - `schema_migrations`, `ar_internal_metadata` — Rails internals
211
+ - `active_storage_*` — ActiveStorage tables
212
+ - `solid_queue_*`, `solid_cache_*`, `solid_cable_*` — Solid* gem internals
213
+
214
+ **Common additions:**
215
+ - `audit_logs`, `versions`, `paper_trail_versions` — Audit trail gems
216
+ - `delayed_jobs` — Delayed::Job queue table
217
+ - `que_jobs` — Que job queue table
218
+ - Internal tables specific to your app
219
+
220
+ ---
221
+
222
+ ### `excluded_job_classes`
223
+
224
+ **Type:** Array of Strings
225
+ **Required:** No
226
+ **Default:** `[]` (uses built-in defaults only)
227
+
228
+ Additional job classes to exclude from capture.
229
+
230
+ **Example:**
231
+ ```ruby
232
+ config.excluded_job_classes = [
233
+ "MyApp::HealthCheckJob",
234
+ "MyApp::MetricsJob",
235
+ "MyApp::HeartbeatJob"
236
+ ]
237
+ ```
238
+
239
+ **Class names:**
240
+ - Use full class name including module namespaces
241
+ - `"MyApp::SomeJob"` not `"SomeJob"`
242
+
243
+ **Built-in exclusions (automatically excluded):**
244
+ - `SidekiqAlive::Worker` — Sidekiq health check
245
+ - `SolidQueue::CleanupJob` — SolidQueue maintenance
246
+ - `SolidQueue::RecurringJob` — SolidQueue scheduler
247
+
248
+ **Common additions:**
249
+ - Health check jobs
250
+ - Metrics collection jobs
251
+ - Heartbeat/ping jobs
252
+ - Internal maintenance jobs
253
+
254
+ ---
255
+
256
+ ## Display Names
257
+
258
+ ### `display_name_for`
259
+
260
+ **Type:** Hash (String → Symbol)
261
+ **Required:** No
262
+ **Default:** `{}` (uses fallback strategy for all models)
263
+
264
+ Configure how to display human-readable names for database records.
265
+
266
+ **Example:**
267
+ ```ruby
268
+ config.display_name_for = {
269
+ "User" => :email,
270
+ "Product" => :name,
271
+ "Order" => :number,
272
+ "Company" => :name
273
+ }
274
+ ```
275
+
276
+ **Key:** Model class name (String)
277
+ **Value:** Attribute name (Symbol)
278
+
279
+ ### How It Works
280
+
281
+ When a database callback fires, the agent resolves a display name:
282
+
283
+ 1. **If configured** for the model → use that attribute
284
+ 2. **Otherwise, try defaults** → `name`, `title`, `number` (in that order)
285
+ 3. **If nothing found** → fall back to `#id`
286
+
287
+ ### Examples
288
+
289
+ **Before configuration:**
290
+ ```
291
+ User created #123
292
+ Product updated #456
293
+ Order deleted #789
294
+ ```
295
+
296
+ **After configuration:**
297
+ ```
298
+ User created 'jessica@example.com'
299
+ Product updated 'Premium Widget'
300
+ Order deleted '#ORD-2025-0789'
301
+ ```
302
+
303
+ ### Important Constraints
304
+
305
+ **✅ Use direct attributes only:**
306
+ ```ruby
307
+ # GOOD
308
+ config.display_name_for = { "User" => :email }
309
+ ```
310
+
311
+ **❌ Never use associations:**
312
+ ```ruby
313
+ # BAD - Triggers database query
314
+ config.display_name_for = { "Order" => :customer_email }
315
+ ```
316
+
317
+ **Why?** Associations trigger additional database queries, violating the agent's non-blocking guarantee.
318
+
319
+ **Performance:** Display names are resolved at capture time using only data already loaded in memory.
320
+
321
+ ---
322
+
323
+ ## Actor Context
324
+
325
+ ### `actor_from_request`
326
+
327
+ **Type:** Lambda/Proc
328
+ **Required:** No
329
+ **Default:** `nil` (actor tracking disabled)
330
+
331
+ Configure how to extract the "who" (actor) from HTTP requests.
332
+
333
+ **Example:**
334
+ ```ruby
335
+ config.actor_from_request = ->(env, controller) {
336
+ return nil unless controller.respond_to?(:current_user)
337
+ user = controller.current_user
338
+ return nil unless user
339
+
340
+ {
341
+ id: user.id.to_s,
342
+ label: user.email
343
+ }
344
+ }
345
+ ```
346
+
347
+ ### Parameters
348
+
349
+ **`env`** (Hash)
350
+ - Rack environment hash
351
+ - Always present
352
+ - Contains request headers, session, etc.
353
+
354
+ **`controller`** (Object or nil)
355
+ - Rails controller instance
356
+ - `nil` if controller not available (e.g., Rack apps, API-only mode)
357
+
358
+ ### Return Value
359
+
360
+ Return one of:
361
+
362
+ **Actor hash:**
363
+ ```ruby
364
+ {
365
+ id: "123", # Required: stable identifier
366
+ label: "user@email.com" # Optional: human-readable display
367
+ }
368
+ ```
369
+
370
+ **Nil (actor unknown):**
371
+ ```ruby
372
+ nil
373
+ ```
374
+
375
+ ### Schema
376
+
377
+ | Field | Type | Required | Description |
378
+ |-------|------|----------|-------------|
379
+ | `id` | String | Yes | Stable identifier (e.g., user ID). Never changes. |
380
+ | `label` | String | No | Human-readable display (e.g., email). Can change. |
381
+
382
+ ### Examples
383
+
384
+ **Devise:**
385
+ ```ruby
386
+ config.actor_from_request = ->(env, controller) {
387
+ return nil unless controller.respond_to?(:current_user)
388
+ user = controller.current_user
389
+ return nil unless user
390
+
391
+ { id: user.id.to_s, label: user.email }
392
+ }
393
+ ```
394
+
395
+ **Clearance:**
396
+ ```ruby
397
+ config.actor_from_request = ->(env, controller) {
398
+ return nil unless controller.respond_to?(:current_user)
399
+ user = controller.current_user
400
+ return nil unless user
401
+
402
+ { id: user.id.to_s, label: user.email }
403
+ }
404
+ ```
405
+
406
+ **Custom session-based auth:**
407
+ ```ruby
408
+ config.actor_from_request = ->(env, controller) {
409
+ user_id = env["rack.session"]&.dig("user_id")
410
+ return nil unless user_id
411
+
412
+ # Only if you have fast caching
413
+ user = User.find_by(id: user_id)
414
+ return nil unless user
415
+
416
+ { id: user.id.to_s, label: user.email }
417
+ }
418
+ ```
419
+
420
+ **API token auth:**
421
+ ```ruby
422
+ config.actor_from_request = ->(env, controller) {
423
+ token = env["HTTP_AUTHORIZATION"]&.split(" ")&.last
424
+ return nil unless token
425
+
426
+ # Lookup user by API token
427
+ user = User.find_by(api_token: token)
428
+ return nil unless user
429
+
430
+ { id: user.id.to_s, label: user.email }
431
+ }
432
+ ```
433
+
434
+ ### Design Philosophy
435
+
436
+ **Actor extraction is opt-in, not automatic.**
437
+
438
+ This prevents:
439
+ - Incorrect attribution (admin impersonating another user)
440
+ - Wrong actors (service accounts, background jobs)
441
+ - Silent failures (custom auth systems)
442
+
443
+ **When actor is unknown, events are captured with `actor: null`.**
444
+
445
+ **Design principle:** Missing data is acceptable; wrong data is not.
446
+
447
+ ---
448
+
449
+ ## Transport Settings
450
+
451
+ ### `buffer_size`
452
+
453
+ **Type:** Integer
454
+ **Required:** No
455
+ **Default:** `10000`
456
+
457
+ Maximum number of events to buffer in memory before oldest events are dropped.
458
+
459
+ **Example:**
460
+ ```ruby
461
+ config.buffer_size = 10000
462
+ ```
463
+
464
+ **Memory usage:**
465
+ - Default (10000 events) ≈ 1MB - 2MB
466
+ - 5000 events ≈ 500KB - 1MB
467
+ - 20000 events ≈ 2MB - 4MB
468
+
469
+ **When to adjust:**
470
+ - **Increase to 20000** if you see "Buffer full, dropping events" warnings
471
+ - **Decrease to 5000** if memory usage is a concern (low-volume apps)
472
+ - **Default (10000) is optimized** for high-volume applications with many background jobs
473
+
474
+ **Buffer behavior:**
475
+ - Circular buffer (oldest events dropped when full)
476
+ - Thread-safe
477
+ - Non-blocking
478
+
479
+ ---
480
+
481
+ ### `send_interval`
482
+
483
+ **Type:** Integer (seconds)
484
+ **Required:** No
485
+ **Default:** `3`
486
+
487
+ How often (in seconds) to flush the buffer and send events to the server.
488
+
489
+ **Example:**
490
+ ```ruby
491
+ config.send_interval = 3
492
+ ```
493
+
494
+ **Trade-offs:**
495
+
496
+ | Value | Latency | Network Usage |
497
+ |-------|---------|---------------|
498
+ | 1s | Lower (more real-time) | Higher (more requests) |
499
+ | 3s | Balanced (default) | Balanced |
500
+ | 5s | Slightly higher latency | Lower requests |
501
+ | 10s | Higher (more delay) | Lower (fewer requests) |
502
+
503
+ **When to adjust:**
504
+ - **Decrease to 1-2s** for more real-time updates
505
+ - **Increase to 5-10s** to reduce network traffic for low-volume apps
506
+ - **Default (3s) is optimized** for high-volume applications with good throughput
507
+
508
+ **Note:** Events are also sent when buffer is full, regardless of interval.
509
+
510
+ ---
511
+
512
+ ### `retry_attempts`
513
+
514
+ **Type:** Integer
515
+ **Required:** No
516
+ **Default:** `3`
517
+
518
+ Number of retry attempts for failed sends (with exponential backoff).
519
+
520
+ **Example:**
521
+ ```ruby
522
+ config.retry_attempts = 3
523
+ ```
524
+
525
+ **Retry schedule (default):**
526
+ 1. First attempt: immediate
527
+ 2. Retry 1: after 1 second
528
+ 3. Retry 2: after 2 seconds
529
+ 4. Retry 3: after 4 seconds
530
+ 5. Give up
531
+
532
+ **When to adjust:**
533
+ - **Increase (4-5)** if network is unreliable
534
+ - **Decrease (1-2)** if you prefer to drop events quickly
535
+ - **Set to 0** to disable retries (not recommended)
536
+
537
+ **After max retries:**
538
+ - Events are dropped
539
+ - Warning is logged
540
+ - Application continues normally
541
+
542
+ ---
543
+
544
+ ## Logging
545
+
546
+ ### `log_level`
547
+
548
+ **Type:** Symbol
549
+ **Required:** No
550
+ **Default:** `:info`
551
+
552
+ Agent log verbosity level.
553
+
554
+ **Example:**
555
+ ```ruby
556
+ config.log_level = :info
557
+ ```
558
+
559
+ **Options:**
560
+
561
+ | Level | What's Logged |
562
+ |-------|---------------|
563
+ | `:debug` | Everything (capture events, buffer state, send attempts) |
564
+ | `:info` | Initialization, sends, warnings (default) |
565
+ | `:warn` | Warnings and errors only |
566
+ | `:error` | Errors only |
567
+
568
+ **Debug output example:**
569
+ ```
570
+ [EzLogsAgent] Captured HTTP event: GET /users (200, 45ms)
571
+ [EzLogsAgent] Captured Database event: User#create (id: 123)
572
+ [EzLogsAgent] Buffer: 12 events
573
+ [EzLogsAgent] Sending batch of 12 events...
574
+ [EzLogsAgent] Batch sent successfully (HTTP 200, 120ms)
575
+ ```
576
+
577
+ **Info output example (default):**
578
+ ```
579
+ [EzLogsAgent] Agent initialized successfully
580
+ [EzLogsAgent] Sending batch of 12 events...
581
+ [EzLogsAgent] Batch sent successfully (HTTP 200)
582
+ ```
583
+
584
+ **When to use debug:**
585
+ - Troubleshooting missing events
586
+ - Verifying capture is working
587
+ - Debugging correlation issues
588
+
589
+ **Note:** Debug level can be verbose in high-traffic applications.
590
+
591
+ ---
592
+
593
+ ## Environment Variables
594
+
595
+ While not directly supported as a configuration option, you can use environment variables for any setting:
596
+
597
+ ### Recommended Pattern
598
+
599
+ ```ruby
600
+ EzLogsAgent.configure do |config|
601
+ config.server_url = ENV.fetch('EZLOGS_SERVER_URL')
602
+ config.project_token = ENV.fetch('EZLOGS_API_KEY')
603
+
604
+ config.capture_http = ENV.fetch('EZLOGS_CAPTURE_HTTP', 'true') == 'true'
605
+ config.capture_jobs = ENV.fetch('EZLOGS_CAPTURE_JOBS', 'true') == 'true'
606
+ config.capture_database = ENV.fetch('EZLOGS_CAPTURE_DATABASE', 'true') == 'true'
607
+
608
+ config.buffer_size = ENV.fetch('EZLOGS_BUFFER_SIZE', '10000').to_i
609
+ config.send_interval = ENV.fetch('EZLOGS_SEND_INTERVAL', '3').to_i
610
+ config.retry_attempts = ENV.fetch('EZLOGS_RETRY_ATTEMPTS', '3').to_i
611
+
612
+ config.log_level = ENV.fetch('EZLOGS_LOG_LEVEL', 'info').to_sym
613
+ end
614
+ ```
615
+
616
+ ### Example `.env` File
617
+
618
+ ```bash
619
+ EZLOGS_SERVER_URL=https://your-ezlogs-server.com
620
+ EZLOGS_API_KEY=ezl_your_api_key_here
621
+
622
+ # Optional
623
+ EZLOGS_CAPTURE_HTTP=true
624
+ EZLOGS_CAPTURE_JOBS=true
625
+ EZLOGS_CAPTURE_DATABASE=true
626
+ EZLOGS_BUFFER_SIZE=10000
627
+ EZLOGS_SEND_INTERVAL=3
628
+ EZLOGS_RETRY_ATTEMPTS=3
629
+ EZLOGS_LOG_LEVEL=info
630
+ ```
631
+
632
+ ---
633
+
634
+ ## Validation
635
+
636
+ Configuration is validated when Rails boots. Errors and warnings are logged.
637
+
638
+ ### Error Messages
639
+
640
+ **Missing server_url:**
641
+ ```
642
+ [Railtie] Configuration validation failed:
643
+ - server_url is required. Set it in config/initializers/ez_logs_agent.rb
644
+ [Railtie] Agent initialization skipped. Please fix configuration errors.
645
+ ```
646
+
647
+ **Invalid server_url:**
648
+ ```
649
+ [Railtie] Configuration validation failed:
650
+ - server_url must start with http:// or https://
651
+ [Railtie] Agent initialization skipped. Please fix configuration errors.
652
+ ```
653
+
654
+ ### Warning Messages
655
+
656
+ **Missing project_token:**
657
+ ```
658
+ [Railtie] Configuration warnings:
659
+ - project_token is not set. Authentication may fail if the server requires it.
660
+ [Railtie] Agent will attempt to initialize without authentication.
661
+ ```
662
+
663
+ ### Validation Rules
664
+
665
+ | Setting | Validation |
666
+ |---------|-----------|
667
+ | `server_url` | Must be present, must start with `http://` or `https://` |
668
+ | `project_token` | Warning if missing (not required but recommended) |
669
+ | `buffer_size` | Must be positive integer |
670
+ | `send_interval` | Must be positive integer |
671
+ | `retry_attempts` | Must be non-negative integer |
672
+ | `log_level` | Must be one of: `:debug`, `:info`, `:warn`, `:error` |
673
+
674
+ **If validation fails:**
675
+ - Agent initialization is skipped
676
+ - Application starts normally (non-blocking)
677
+ - Fix errors and restart Rails
678
+
679
+ ---
680
+
681
+ ## Complete Example
682
+
683
+ Here's a fully configured example with all options:
684
+
685
+ ```ruby
686
+ EzLogsAgent.configure do |config|
687
+ # ==========================================
688
+ # Required
689
+ # ==========================================
690
+ config.server_url = ENV.fetch('EZLOGS_SERVER_URL')
691
+ config.project_token = ENV.fetch('EZLOGS_API_KEY')
692
+
693
+ # ==========================================
694
+ # Event Capture
695
+ # ==========================================
696
+ config.capture_http = true
697
+ config.capture_jobs = true
698
+ config.capture_database = true
699
+
700
+ # ==========================================
701
+ # Exclusions
702
+ # ==========================================
703
+ config.excluded_paths = ["/admin*", "/internal*"]
704
+ config.excluded_tables = ["audit_logs", "versions"]
705
+ config.excluded_job_classes = ["MyApp::HealthCheckJob"]
706
+
707
+ # ==========================================
708
+ # Display Names
709
+ # ==========================================
710
+ config.display_name_for = {
711
+ "User" => :email,
712
+ "Product" => :name,
713
+ "Order" => :number
714
+ }
715
+
716
+ # ==========================================
717
+ # Actor Context
718
+ # ==========================================
719
+ config.actor_from_request = ->(env, controller) {
720
+ return nil unless controller.respond_to?(:current_user)
721
+ user = controller.current_user
722
+ return nil unless user
723
+
724
+ { id: user.id.to_s, label: user.email }
725
+ }
726
+
727
+ # ==========================================
728
+ # Transport
729
+ # ==========================================
730
+ config.buffer_size = 10000
731
+ config.send_interval = 3
732
+ config.retry_attempts = 3
733
+
734
+ # ==========================================
735
+ # Logging
736
+ # ==========================================
737
+ config.log_level = :info
738
+ end
739
+ ```
740
+
741
+ ---
742
+
743
+ ## See Also
744
+
745
+ - [QUICKSTART.md](QUICKSTART.md) — Getting started guide
746
+ - [README.md](README.md) — Full documentation
747
+ - [FAQ.md](FAQ.md) — Frequently asked questions
748
+ - [Troubleshooting](README.md#troubleshooting) — Common issues and solutions
749
+
750
+ ---
751
+
752
+ **Questions?** [Open an issue](https://github.com/your-org/ez_logs/issues) or email support@ezlogs.com