rails_semantic_logging 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +254 -0
- data/lib/rails_semantic_logging/action_controller/default_payload.rb +28 -0
- data/lib/rails_semantic_logging/configuration.rb +74 -0
- data/lib/rails_semantic_logging/datadog/log_injection.rb +36 -0
- data/lib/rails_semantic_logging/formatters/datadog.rb +189 -0
- data/lib/rails_semantic_logging/job_logging/active_job_patch.rb +20 -0
- data/lib/rails_semantic_logging/job_logging/sidekiq_patch.rb +17 -0
- data/lib/rails_semantic_logging/railtie.rb +76 -0
- data/lib/rails_semantic_logging/rspec/helpers.rb +84 -0
- data/lib/rails_semantic_logging/rspec/matchers.rb +120 -0
- data/lib/rails_semantic_logging/version.rb +3 -0
- data/lib/rails_semantic_logging.rb +9 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 489018fd333371c700b41e42079eedb52ecae7560a0d0fbecd2f354b62cd4a85
|
|
4
|
+
data.tar.gz: 9d3b301a0275c7b0fa18348192413c8d7359fa4dbccac5ffbb6225d04e753187
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bf98ed4b1007f7a3085a0ea1d75bcc6c5a8158bd94ee2e246ec39ebb0e9934d89ee3b4287fc70f74aeff969e11e8c135d5c588940e72e34e37fc2539d5a4d8f9
|
|
7
|
+
data.tar.gz: bad27c3d7210034773745b1f8889ecba556af9db932e7c762cd69a86cc322449b90f2baf1a62285bcee583ca8c1d3c640a1369d2d5c906983a44a62cd6662dfd
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Fabio Napoleoni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# RailsSemanticLogging
|
|
2
|
+
|
|
3
|
+
Opinionated Rails semantic logger configuration with Datadog support. Provides a consistent, structured JSON logging setup for Rails applications, with hooks for Sidekiq, ActiveJob, and Datadog-friendly formatters.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Datadog formatter** with [Standard Attributes](https://docs.datadoghq.com/standard-attributes/) mapping
|
|
8
|
+
- **Default payload enrichment** for controllers (host, user_agent, referer) mapped to `http.*`
|
|
9
|
+
- **JSON formatter** for structured logging without Datadog-specific fields
|
|
10
|
+
- **ActiveJob integration** with named tags (`job_class`, `job_id`, `queue`) instead of array tags
|
|
11
|
+
- **Sidekiq integration** with job context in all log lines
|
|
12
|
+
- **Configurable** via [anyway_config](https://github.com/palkan/anyway_config) (YAML, env vars, or code)
|
|
13
|
+
- **Environment-aware** defaults (Datadog JSON in production, color in development, fatal in test)
|
|
14
|
+
- **RSpec matcher** for asserting log output in tests
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add to your application's Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'rails_semantic_logging'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run `bundle install`.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Basic Configuration
|
|
29
|
+
|
|
30
|
+
The gem auto-configures via a Railtie when loaded by Rails. Out of the box:
|
|
31
|
+
|
|
32
|
+
- Datadog JSON formatter in production, color formatter elsewhere
|
|
33
|
+
- `request_id` and `client_ip` log tags
|
|
34
|
+
- Default payload enrichment (host, user_agent, referer) on all controller actions
|
|
35
|
+
- Quiet assets logging
|
|
36
|
+
- Sync mode in test environment
|
|
37
|
+
- Log level: INFO (production), DEBUG (development), FATAL (test)
|
|
38
|
+
- `LOG_LEVEL` env var override supported
|
|
39
|
+
|
|
40
|
+
### Custom Configuration
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# config/application.rb (inside class body)
|
|
44
|
+
RailsSemanticLogging.configure do |config|
|
|
45
|
+
config.application_name = 'My App'
|
|
46
|
+
config.environment_name = ENV.fetch('NAMESPACE', Rails.env)
|
|
47
|
+
|
|
48
|
+
# Add custom log tags (merged with default request_id + client_ip)
|
|
49
|
+
config.custom_log_tags = {
|
|
50
|
+
user: ->(request) { extract_user_id(request) },
|
|
51
|
+
tenant: ->(request) { request.headers['X-Tenant-ID'] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Override formatters (default: :datadog for production, :color for development)
|
|
55
|
+
# config.production_formatter = :json # plain JSON without Datadog mapping
|
|
56
|
+
# config.production_formatter = :datadog # Datadog Standard Attributes (default)
|
|
57
|
+
# config.development_formatter = :color # colorized console output (default)
|
|
58
|
+
|
|
59
|
+
# Disable automatic payload enrichment on controllers (default: true)
|
|
60
|
+
# config.default_payload = false
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Configuration can also be set via YAML (`config/rails_semantic_logging.yml`) or environment variables (`RAILS_SEMANTIC_LOGGING_QUIET_ASSETS=false`) thanks to [anyway_config](https://github.com/palkan/anyway_config).
|
|
65
|
+
|
|
66
|
+
#### Formatter override via YAML
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# config/rails_semantic_logging.yml
|
|
70
|
+
production:
|
|
71
|
+
production_formatter: json
|
|
72
|
+
|
|
73
|
+
development:
|
|
74
|
+
development_formatter: color
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Formatter override via environment variables
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
RAILS_SEMANTIC_LOGGING_PRODUCTION_FORMATTER=datadog bin/rails server
|
|
81
|
+
RAILS_SEMANTIC_LOGGING_DEVELOPMENT_FORMATTER=color bin/rails server
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Both string and symbol values are accepted for formatter options (e.g. `"datadog"` from YAML/ENV is equivalent to `:datadog` in Ruby).
|
|
85
|
+
|
|
86
|
+
### Configuration Options
|
|
87
|
+
|
|
88
|
+
| Option | Default | Description |
|
|
89
|
+
|--------|---------|-------------|
|
|
90
|
+
| `application_name` | Rails app name | SemanticLogger application name |
|
|
91
|
+
| `environment_name` | `Rails.env` | SemanticLogger environment name |
|
|
92
|
+
| `custom_log_tags` | `{}` | Extra log tags (merged with `request_id` + `client_ip`) |
|
|
93
|
+
| `quiet_assets` | `true` | Silence asset pipeline logs |
|
|
94
|
+
| `sync_in_test` | `true` | Use synchronous logging in test environment |
|
|
95
|
+
| `default_payload` | `true` | Auto-include host, user_agent, referer in controller logs |
|
|
96
|
+
| `production_formatter` | `:datadog` | Formatter for production (`:datadog`, `:json`, or instance) |
|
|
97
|
+
| `development_formatter` | `:color` | Formatter for non-production environments |
|
|
98
|
+
|
|
99
|
+
## Datadog Integration
|
|
100
|
+
|
|
101
|
+
### Standard Attributes Mapping
|
|
102
|
+
|
|
103
|
+
The `:datadog` formatter maps all log fields to [Datadog Standard Attributes](https://docs.datadoghq.com/standard-attributes/) so logs are automatically parsed and correlated in Datadog without custom pipelines.
|
|
104
|
+
|
|
105
|
+
#### Core log fields
|
|
106
|
+
|
|
107
|
+
| SemanticLogger field | Datadog Standard Attribute | Notes |
|
|
108
|
+
|---------------------|---------------------------|-------|
|
|
109
|
+
| Logger name | `logger.name` | Class/module that emitted the log |
|
|
110
|
+
| Level | `status` | `debug`, `info`, `warn`, `error`, `fatal` |
|
|
111
|
+
| Duration | `duration` | Converted to **nanoseconds** |
|
|
112
|
+
| Exception class | `error.kind` | e.g. `RuntimeError` |
|
|
113
|
+
| Exception message | `error.message` | Human-readable description |
|
|
114
|
+
| Backtrace | `error.stack` | Full stack trace as string |
|
|
115
|
+
|
|
116
|
+
#### HTTP fields (from controller payload)
|
|
117
|
+
|
|
118
|
+
When `default_payload` is enabled (default), controller request logs include:
|
|
119
|
+
|
|
120
|
+
| Payload field | Datadog Standard Attribute | Source |
|
|
121
|
+
|--------------|---------------------------|--------|
|
|
122
|
+
| `status` | `http.status_code` | Rails built-in |
|
|
123
|
+
| `method` | `http.method` | Rails built-in |
|
|
124
|
+
| `path` | `http.url` | Rails built-in |
|
|
125
|
+
| `host` | `http.url_details.host` | `DefaultPayload` concern |
|
|
126
|
+
| `user_agent` | `http.useragent` | `DefaultPayload` concern |
|
|
127
|
+
| `referer` | `http.referer` | `DefaultPayload` concern |
|
|
128
|
+
|
|
129
|
+
#### Datadog trace correlation
|
|
130
|
+
|
|
131
|
+
When Datadog tracing is active, the formatter injects: `dd.trace_id`, `dd.span_id`, `dd.env`, `dd.service`, `dd.version`.
|
|
132
|
+
|
|
133
|
+
### Example: Complete request log (production)
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"timestamp": "2025-10-26T10:30:45.123Z",
|
|
138
|
+
"status": "info",
|
|
139
|
+
"host": "web-01",
|
|
140
|
+
"logger.name": "Rails",
|
|
141
|
+
"message": "Completed 200 OK in 42ms",
|
|
142
|
+
"duration": 42500000,
|
|
143
|
+
"http.status_code": 200,
|
|
144
|
+
"http.method": "GET",
|
|
145
|
+
"http.url": "/api/v1/bikes",
|
|
146
|
+
"http.url_details.host": "api.example.com",
|
|
147
|
+
"http.useragent": "Mozilla/5.0 (iPhone; iOS 17.0)",
|
|
148
|
+
"http.referer": "https://app.example.com/dashboard",
|
|
149
|
+
"dd.trace_id": "1234567890",
|
|
150
|
+
"dd.span_id": "9876543210"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Example: Background job log
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"timestamp": "2025-10-26T10:31:00.456Z",
|
|
159
|
+
"status": "info",
|
|
160
|
+
"host": "worker-01",
|
|
161
|
+
"logger.name": "Rails",
|
|
162
|
+
"message": "Performing ImportBikesJob from Sidekiq(default)",
|
|
163
|
+
"named_tags": {
|
|
164
|
+
"job_class": "ImportBikesJob",
|
|
165
|
+
"job_id": "abc-123",
|
|
166
|
+
"queue": "default"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Example: Error log
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"timestamp": "2025-10-26T10:32:15.789Z",
|
|
176
|
+
"status": "error",
|
|
177
|
+
"host": "web-01",
|
|
178
|
+
"logger.name": "BikeService",
|
|
179
|
+
"message": "Failed to import bike",
|
|
180
|
+
"error.kind": "ActiveRecord::RecordInvalid",
|
|
181
|
+
"error.message": "Validation failed: VIN is not unique",
|
|
182
|
+
"error.stack": "app/services/bike_service.rb:42:in `import'\n..."
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Formatters
|
|
187
|
+
|
|
188
|
+
### JSON Formatter `:json`
|
|
189
|
+
|
|
190
|
+
Plain structured JSON without Datadog-specific field mapping. Useful for non-Datadog log pipelines.
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"timestamp": "2025-10-26T10:30:45.123Z",
|
|
195
|
+
"level": "info",
|
|
196
|
+
"host": "server-1",
|
|
197
|
+
"name": "Rails",
|
|
198
|
+
"message": "Processing request",
|
|
199
|
+
"duration_ms": 42.5
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Color Formatter `:color` (development default)
|
|
204
|
+
|
|
205
|
+
Uses the built-in `SemanticLogger::Formatters::Color` for human-readable development output.
|
|
206
|
+
|
|
207
|
+
## RSpec Matcher
|
|
208
|
+
|
|
209
|
+
Include the matcher module in your spec config:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
require 'rails_semantic_logging/rspec/matchers'
|
|
213
|
+
|
|
214
|
+
RSpec.configure do |config|
|
|
215
|
+
config.include RailsSemanticLogging::RSpec::Matchers
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Usage:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
expect { logger.info("hello") }.to log_semantic(level: :info, message: /hello/)
|
|
223
|
+
expect { logger.warn("oops", key: "val") }.to log_semantic(payload: { key: "val" })
|
|
224
|
+
expect { do_work }.to log_semantic(named_tags: { job_class: 'MyJob' })
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Puma Integration
|
|
228
|
+
|
|
229
|
+
When using Puma in clustered mode, reopen log appenders after forking:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
# config/puma.rb
|
|
233
|
+
if workers_number.positive?
|
|
234
|
+
preload_app!
|
|
235
|
+
|
|
236
|
+
before_worker_boot do
|
|
237
|
+
SemanticLogger.reopen
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
git clone https://github.com/fabn/rails_semantic_logging.git
|
|
246
|
+
cd rails_semantic_logging
|
|
247
|
+
bundle install
|
|
248
|
+
bundle exec rspec
|
|
249
|
+
bundle exec rubocop
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module RailsSemanticLogging
|
|
2
|
+
module ActionController
|
|
3
|
+
# Concern that enriches controller log payload with standard HTTP request fields.
|
|
4
|
+
# Include in ApplicationController (or a base controller) to automatically add
|
|
5
|
+
# host, user_agent, and referer to every request log line.
|
|
6
|
+
#
|
|
7
|
+
# These fields are then mapped to Datadog Standard Attributes by the Datadog formatter:
|
|
8
|
+
# host -> http.url_details.host
|
|
9
|
+
# user_agent -> http.useragent
|
|
10
|
+
# referer -> http.referer
|
|
11
|
+
# status -> http.status_code (already in Rails payload)
|
|
12
|
+
# method -> http.method (already in Rails payload)
|
|
13
|
+
# path -> http.url (already in Rails payload)
|
|
14
|
+
module DefaultPayload
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
# @see https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal/instrumentation.rb
|
|
18
|
+
def append_info_to_payload(payload)
|
|
19
|
+
super
|
|
20
|
+
# Use :full_path because rails_semantic_logger strips query string from :path
|
|
21
|
+
payload[:full_path] = request.fullpath
|
|
22
|
+
payload[:host] = request.host
|
|
23
|
+
payload[:user_agent] = request.user_agent
|
|
24
|
+
payload[:referer] = request.referer if request.referer.present?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'anyway_config'
|
|
2
|
+
|
|
3
|
+
module RailsSemanticLogging
|
|
4
|
+
class Configuration < Anyway::Config
|
|
5
|
+
config_name :rails_semantic_logging
|
|
6
|
+
|
|
7
|
+
attr_config(
|
|
8
|
+
application_name: nil,
|
|
9
|
+
environment_name: nil,
|
|
10
|
+
custom_log_tags: {},
|
|
11
|
+
quiet_assets: true,
|
|
12
|
+
sync_in_test: true,
|
|
13
|
+
stdout_sync: true,
|
|
14
|
+
default_payload: true,
|
|
15
|
+
production_formatter: nil,
|
|
16
|
+
development_formatter: :color
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_LOG_TAGS = { request_id: :request_id, client_ip: :remote_ip }.freeze
|
|
20
|
+
|
|
21
|
+
VALID_LOG_LEVELS = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
|
|
22
|
+
|
|
23
|
+
# Merges built-in default tags with app-provided custom tags
|
|
24
|
+
def effective_log_tags
|
|
25
|
+
DEFAULT_LOG_TAGS.merge(custom_log_tags)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the appropriate formatter for the given environment
|
|
29
|
+
def formatter_for(env)
|
|
30
|
+
case env.to_s
|
|
31
|
+
when 'production'
|
|
32
|
+
resolve_formatter(production_formatter) || RailsSemanticLogging::Formatters::Datadog.new
|
|
33
|
+
else
|
|
34
|
+
resolve_formatter(development_formatter) || :color
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the appropriate log level for the given environment, respecting LOG_LEVEL env var
|
|
39
|
+
def log_level_for(env)
|
|
40
|
+
default_level = case env.to_s
|
|
41
|
+
when 'development' then 'DEBUG'
|
|
42
|
+
when 'test' then 'FATAL'
|
|
43
|
+
else 'INFO'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
requested = ENV.fetch('LOG_LEVEL', default_level).to_s.upcase
|
|
47
|
+
VALID_LOG_LEVELS.include?(requested) ? requested : default_level
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def resolve_formatter(value)
|
|
53
|
+
value = value.to_sym if value.is_a?(String)
|
|
54
|
+
return value unless value.is_a?(Symbol)
|
|
55
|
+
return RailsSemanticLogging::Formatters::Datadog.new if value == :datadog
|
|
56
|
+
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
def config
|
|
63
|
+
@config ||= Configuration.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configure
|
|
67
|
+
yield(config)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reset_config!
|
|
71
|
+
@config = Configuration.new
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Monkey patch for Datadog's ActiveJob LogInjection to use hash-style
|
|
2
|
+
# correlation tags instead of string-style. This is necessary because
|
|
3
|
+
# we patch ActiveJob::Logging to use named tags (job_class, job_id, queue),
|
|
4
|
+
# so Datadog must also use hash-style tags for compatibility with SemanticLogger.
|
|
5
|
+
|
|
6
|
+
module RailsSemanticLogging
|
|
7
|
+
module Datadog
|
|
8
|
+
module LogInjection
|
|
9
|
+
def self.apply!
|
|
10
|
+
return unless defined?(::Datadog::Tracing::Contrib::ActiveJob::LogInjection)
|
|
11
|
+
|
|
12
|
+
# Ensure the SemanticLogger ActiveJob extension is loaded first
|
|
13
|
+
require 'rails_semantic_logger/extensions/active_job/logging'
|
|
14
|
+
|
|
15
|
+
# Replace the original module with our patched version
|
|
16
|
+
::Datadog::Tracing::Contrib::ActiveJob.send(:remove_const, :LogInjection)
|
|
17
|
+
::Datadog::Tracing::Contrib::ActiveJob.const_set(:LogInjection, PatchedLogInjection)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Replacement module that uses correlation.to_h instead of log_correlation
|
|
21
|
+
module PatchedLogInjection
|
|
22
|
+
def self.included(base)
|
|
23
|
+
base.class_eval do
|
|
24
|
+
around_perform do |_, block|
|
|
25
|
+
if ::Datadog.configuration.tracing.log_injection && logger.respond_to?(:tagged)
|
|
26
|
+
logger.tagged(::Datadog::Tracing.correlation.to_h, &block)
|
|
27
|
+
else
|
|
28
|
+
block.call
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require 'semantic_logger'
|
|
2
|
+
|
|
3
|
+
module RailsSemanticLogging
|
|
4
|
+
module Formatters
|
|
5
|
+
# Datadog-optimized JSON formatter that maps log fields to
|
|
6
|
+
# Datadog Standard Attributes (https://docs.datadoghq.com/standard-attributes/).
|
|
7
|
+
#
|
|
8
|
+
# Key mappings:
|
|
9
|
+
# name -> logger.name
|
|
10
|
+
# level -> status
|
|
11
|
+
# duration -> duration (nanoseconds) + duration_human (Rails format)
|
|
12
|
+
# exception -> error: { kind, message, stack }
|
|
13
|
+
# payload -> http: { status_code, method, url, ... } (controller requests)
|
|
14
|
+
# named_tags.dd -> dd (top-level, for trace linking)
|
|
15
|
+
# named_tags.user_* -> usr.{id, email, name, role}
|
|
16
|
+
class Datadog < ::SemanticLogger::Formatters::Raw
|
|
17
|
+
NANOSECONDS_PER_MILLISECOND = 1_000_000
|
|
18
|
+
|
|
19
|
+
# Mapping of Rails payload keys to Datadog http standard attribute names
|
|
20
|
+
HTTP_PAYLOAD_MAP = {
|
|
21
|
+
status: :status_code,
|
|
22
|
+
method: :method,
|
|
23
|
+
host: :host,
|
|
24
|
+
user_agent: :useragent,
|
|
25
|
+
referer: :referer
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Mapping of user-related named_tags to usr.* standard attributes
|
|
29
|
+
USER_NAMED_TAGS_MAP = {
|
|
30
|
+
user_id: :id,
|
|
31
|
+
user_email: :email,
|
|
32
|
+
user_name: :name,
|
|
33
|
+
user_role: :role
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def initialize(time_format: :iso_8601, time_key: :timestamp, **args) # rubocop:disable Naming/VariableNumber
|
|
37
|
+
super(time_format:, time_key:, log_application: false, log_host: true, log_environment: false, **args)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(log, logger)
|
|
41
|
+
super
|
|
42
|
+
remap_named_tags
|
|
43
|
+
remap_http_payload
|
|
44
|
+
parse_url_details
|
|
45
|
+
apache_message
|
|
46
|
+
deep_compact_blank!(hash)
|
|
47
|
+
hash.to_json
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def thread_name
|
|
51
|
+
# Exclude thread_name from output
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def name
|
|
55
|
+
hash[:'logger.name'] = log.name if log.name
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def level
|
|
59
|
+
hash[:status] = log.level
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def duration
|
|
63
|
+
# Propagate duration from payload if not set on log
|
|
64
|
+
log.duration = log.payload[:duration] if log.duration.nil? && log.payload&.dig(:duration)
|
|
65
|
+
return unless log.duration
|
|
66
|
+
|
|
67
|
+
# Datadog standard: duration in nanoseconds
|
|
68
|
+
hash[:duration] = (log.duration * NANOSECONDS_PER_MILLISECOND).to_i
|
|
69
|
+
# Human-readable duration for readability (Rails format)
|
|
70
|
+
hash[:duration_human] = "#{log.duration.round(2)}ms"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def exception
|
|
74
|
+
return unless log.exception
|
|
75
|
+
|
|
76
|
+
hash[:error] = {
|
|
77
|
+
kind: log.exception.class.name,
|
|
78
|
+
message: log.exception.message,
|
|
79
|
+
stack: log.exception.backtrace&.join("\n")
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Parses http.url into url_details with host, path and queryString
|
|
86
|
+
def parse_url_details
|
|
87
|
+
return unless hash.dig(:http, :url)
|
|
88
|
+
|
|
89
|
+
url = hash[:http][:url]
|
|
90
|
+
path, query = url.split('?', 2)
|
|
91
|
+
details = { path: path }
|
|
92
|
+
details[:queryString] = Rack::Utils.parse_query(query) if query.present?
|
|
93
|
+
# Datadog standard: host belongs under http.url_details
|
|
94
|
+
details[:host] = hash[:http].delete(:host) if hash[:http][:host]
|
|
95
|
+
|
|
96
|
+
hash[:http][:url_details] = details
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# For completed ActionController requests, replace the message with an
|
|
100
|
+
# Apache-like format: "GET /path JSON 200 1.17ms" for better readability on mobile
|
|
101
|
+
def apache_message
|
|
102
|
+
return unless hash[:http].is_a?(Hash) && hash[:http][:status_code] && log.duration
|
|
103
|
+
|
|
104
|
+
method = hash[:http][:method] || '-'
|
|
105
|
+
url = hash[:http][:url] || '-'
|
|
106
|
+
status = hash[:http][:status_code]
|
|
107
|
+
format = hash[:payload].is_a?(Hash) ? hash[:payload][:format] : nil
|
|
108
|
+
|
|
109
|
+
parts = [method, url]
|
|
110
|
+
parts << format if format.present?
|
|
111
|
+
parts << status
|
|
112
|
+
parts << "#{log.duration.round(2)}ms"
|
|
113
|
+
hash[:message] = parts.join(' ')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Remaps known named_tags to Datadog standard attributes.
|
|
117
|
+
# Handles: client_ip, request_id, dd correlation, user_* tags.
|
|
118
|
+
def remap_named_tags
|
|
119
|
+
return unless hash[:named_tags].is_a?(Hash)
|
|
120
|
+
|
|
121
|
+
remap_network_and_request
|
|
122
|
+
remap_dd_correlation
|
|
123
|
+
remap_user_tags
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def remap_network_and_request
|
|
127
|
+
if (ip = hash[:named_tags].delete(:client_ip))
|
|
128
|
+
hash[:network] = { client: { ip: ip } }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return unless (rid = hash[:named_tags].delete(:request_id))
|
|
132
|
+
|
|
133
|
+
hash[:http] ||= {}
|
|
134
|
+
hash[:http][:request_id] = rid
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Lifts Datadog correlation from named_tags (added by dd-trace-rb SemanticLogger
|
|
138
|
+
# instrumentation via Tracing.correlation.to_h) to top-level for trace linking.
|
|
139
|
+
# Skips the dd block entirely when there's no active trace (trace_id is "0").
|
|
140
|
+
def remap_dd_correlation
|
|
141
|
+
dd = hash[:named_tags].delete(:dd)
|
|
142
|
+
ddsource = hash[:named_tags].delete(:ddsource)
|
|
143
|
+
|
|
144
|
+
return unless dd.is_a?(Hash) && dd[:trace_id].to_s != '0'
|
|
145
|
+
|
|
146
|
+
hash[:dd] = dd
|
|
147
|
+
hash[:ddsource] = ddsource if ddsource
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Maps user_* named_tags to usr.* Datadog standard attributes
|
|
151
|
+
def remap_user_tags
|
|
152
|
+
usr = USER_NAMED_TAGS_MAP.each_with_object({}) do |(source, target), h|
|
|
153
|
+
value = hash[:named_tags].delete(source)
|
|
154
|
+
h[target] = value if value.present?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
hash[:usr] = usr if usr.present?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Remaps known HTTP payload fields to nested Datadog http standard attributes
|
|
161
|
+
def remap_http_payload
|
|
162
|
+
return unless hash[:payload].is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
http = HTTP_PAYLOAD_MAP.each_with_object({}) do |(source, target), h|
|
|
165
|
+
h[target] = hash[:payload].delete(source) if hash[:payload].key?(source)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Prefer full_path (with query string) over path (stripped by rails_semantic_logger).
|
|
169
|
+
# Delete both keys unconditionally to avoid duplication in payload.
|
|
170
|
+
full_path = hash[:payload].delete(:full_path)
|
|
171
|
+
path = hash[:payload].delete(:path)
|
|
172
|
+
url = full_path || path
|
|
173
|
+
http[:url] = url if url.present?
|
|
174
|
+
return if http.blank?
|
|
175
|
+
|
|
176
|
+
hash[:http].is_a?(Hash) ? hash[:http].merge!(http) : hash[:http] = http
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Recursively removes blank values from a hash
|
|
180
|
+
def deep_compact_blank!(h)
|
|
181
|
+
h.each do |key, value|
|
|
182
|
+
deep_compact_blank!(value) if value.is_a?(Hash)
|
|
183
|
+
h.delete(key) if value.blank?
|
|
184
|
+
end
|
|
185
|
+
h
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'active_support/concern'
|
|
2
|
+
|
|
3
|
+
module RailsSemanticLogging
|
|
4
|
+
module JobLogging
|
|
5
|
+
# ActiveJob patch to provide named tags instead of array tags.
|
|
6
|
+
# Converts the default tag_logger(class, id) call into named tags
|
|
7
|
+
# (job_class, job_id, queue) for structured logging.
|
|
8
|
+
module ActiveJobPatch
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
def tag_logger(job_class = nil, job_id = nil, &)
|
|
12
|
+
if job_class && job_id
|
|
13
|
+
super(job_class: job_class, job_id: job_id, queue: queue_name, &)
|
|
14
|
+
else
|
|
15
|
+
super(&)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module RailsSemanticLogging
|
|
2
|
+
module JobLogging
|
|
3
|
+
# Sidekiq patch to provide job context in every processed job log line.
|
|
4
|
+
# SemanticLogger uses its own thread for formatting, so Sidekiq::Context
|
|
5
|
+
# (stored in Thread.current) is lost. This patch wraps each job with
|
|
6
|
+
# SemanticLogger named tags for consistent structured output.
|
|
7
|
+
module SidekiqPatch
|
|
8
|
+
# @param [Hash] item Sidekiq job hash
|
|
9
|
+
# @param [String] queue Queue name
|
|
10
|
+
def call(item, queue)
|
|
11
|
+
Sidekiq.logger.tagged(job_class: item['class'], job_id: item['jid'], queue: queue) do
|
|
12
|
+
super(item, queue)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require 'rails/railtie'
|
|
2
|
+
require 'rails_semantic_logger'
|
|
3
|
+
|
|
4
|
+
module RailsSemanticLogging
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
config.before_configuration do
|
|
7
|
+
$stdout.sync = true if RailsSemanticLogging.config.stdout_sync
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Runs BEFORE the upstream rails_semantic_logger Engine's :initialize_logger initializer.
|
|
11
|
+
# This ensures our configuration is applied before the logger is set up.
|
|
12
|
+
initializer 'rails_semantic_logging.configure', before: :initialize_logger do |app|
|
|
13
|
+
cfg = RailsSemanticLogging.config
|
|
14
|
+
|
|
15
|
+
# Configure rails_semantic_logger options
|
|
16
|
+
app.config.rails_semantic_logger.quiet_assets = cfg.quiet_assets
|
|
17
|
+
app.config.rails_semantic_logger.console_logger = false
|
|
18
|
+
app.config.rails_semantic_logger.add_file_appender = false
|
|
19
|
+
|
|
20
|
+
# Set formatter based on environment
|
|
21
|
+
app.config.rails_semantic_logger.format = cfg.formatter_for(Rails.env)
|
|
22
|
+
|
|
23
|
+
# Merge default tags (request_id, client_ip) with app-specific custom tags
|
|
24
|
+
app.config.log_tags = cfg.effective_log_tags
|
|
25
|
+
|
|
26
|
+
# Set log level based on environment, respecting LOG_LEVEL env var
|
|
27
|
+
app.config.log_level = cfg.log_level_for(Rails.env)
|
|
28
|
+
|
|
29
|
+
# Add stdout appender with the configured formatter.
|
|
30
|
+
# IMPORTANT: Do NOT pass level: parameter. Subscriber#level defaults to :trace
|
|
31
|
+
# when unset, which is required by the host app's spec/support/output.rb check.
|
|
32
|
+
app.config.semantic_logger.add_appender(io: $stdout, formatter: app.config.rails_semantic_logger.format)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
config.to_prepare do
|
|
36
|
+
cfg = RailsSemanticLogging.config
|
|
37
|
+
SemanticLogger.application = cfg.application_name || Rails.application.class.module_parent_name
|
|
38
|
+
SemanticLogger.environment = cfg.environment_name || Rails.env
|
|
39
|
+
SemanticLogger.sync! if Rails.env.test? && cfg.sync_in_test
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
config.after_initialize do
|
|
43
|
+
# Include DefaultPayload in ActionController to enrich request logs
|
|
44
|
+
# with host, user_agent, and referer (mapped to http.* by Datadog formatter)
|
|
45
|
+
if RailsSemanticLogging.config.default_payload
|
|
46
|
+
require 'rails_semantic_logging/action_controller/default_payload'
|
|
47
|
+
|
|
48
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
49
|
+
include RailsSemanticLogging::ActionController::DefaultPayload
|
|
50
|
+
end
|
|
51
|
+
ActiveSupport.on_load(:action_controller_api) do
|
|
52
|
+
include RailsSemanticLogging::ActionController::DefaultPayload
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Apply ActiveJob logging patch for named tags
|
|
57
|
+
ActiveSupport.on_load(:active_job) do
|
|
58
|
+
require 'rails_semantic_logging/job_logging/active_job_patch'
|
|
59
|
+
prepend RailsSemanticLogging::JobLogging::ActiveJobPatch
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Apply Sidekiq logging patch if Sidekiq is loaded
|
|
63
|
+
if defined?(::Sidekiq::JobLogger)
|
|
64
|
+
require 'sidekiq/job_logger'
|
|
65
|
+
require 'rails_semantic_logging/job_logging/sidekiq_patch'
|
|
66
|
+
::Sidekiq::JobLogger.prepend(RailsSemanticLogging::JobLogging::SidekiqPatch)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Apply Datadog log injection patch if Datadog tracing is loaded
|
|
70
|
+
if defined?(::Datadog::Tracing::Contrib::ActiveJob::LogInjection)
|
|
71
|
+
require 'rails_semantic_logging/datadog/log_injection'
|
|
72
|
+
RailsSemanticLogging::Datadog::LogInjection.apply!
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module RailsSemanticLogging
|
|
2
|
+
module RSpec
|
|
3
|
+
# Test helpers for applications using RailsSemanticLogging.
|
|
4
|
+
#
|
|
5
|
+
# Provides standalone helpers that work with or without TestProf:
|
|
6
|
+
# - LoggingHelpers: with_logging / with_ar_logging (uses SemanticLogger.silence)
|
|
7
|
+
# - SilenceOutput: capture stdout in tests
|
|
8
|
+
# - Appender validation (ensures single appender at trace level)
|
|
9
|
+
# - LOG env var support (LOG=all or LOG=ar to enable logging in tests)
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# require 'rails_semantic_logging/rspec/helpers'
|
|
13
|
+
# RailsSemanticLogging::RSpec::Helpers.install!
|
|
14
|
+
#
|
|
15
|
+
module Helpers
|
|
16
|
+
# Standalone logging helpers using SemanticLogger.silence.
|
|
17
|
+
# Works without TestProf. If TestProf is present, also patches
|
|
18
|
+
# TestProf::Rails::LoggingHelpers for compatibility.
|
|
19
|
+
module LoggingHelpers
|
|
20
|
+
def with_logging(level = :trace, &)
|
|
21
|
+
SemanticLogger.silence(level, &)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def with_ar_logging(level = :trace, &)
|
|
25
|
+
SemanticLogger.appenders.first.filter = ->(log) { log.name == 'ActiveRecord' }
|
|
26
|
+
SemanticLogger.silence(level, &)
|
|
27
|
+
ensure
|
|
28
|
+
SemanticLogger.appenders.first.filter = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Helper to silence stdout output in tests
|
|
33
|
+
module SilenceOutput
|
|
34
|
+
def silence_stdout
|
|
35
|
+
original_stdout = $stdout
|
|
36
|
+
$stdout = StringIO.new
|
|
37
|
+
yield
|
|
38
|
+
ensure
|
|
39
|
+
$stdout = original_stdout
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Installs all test helpers into RSpec configuration.
|
|
45
|
+
def install!
|
|
46
|
+
configure_rspec!
|
|
47
|
+
patch_test_prof!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def configure_rspec! # rubocop:disable Metrics/MethodLength
|
|
53
|
+
::RSpec.configure do |config|
|
|
54
|
+
# Make logging helpers available in all specs
|
|
55
|
+
config.include LoggingHelpers
|
|
56
|
+
|
|
57
|
+
# Validate appender configuration
|
|
58
|
+
config.before(:suite) do
|
|
59
|
+
if SemanticLogger.appenders.size != 1 || SemanticLogger.appenders.first.level != :trace
|
|
60
|
+
raise 'Expected only one appender with trace level, ' \
|
|
61
|
+
"got #{SemanticLogger.appenders.size} with #{SemanticLogger.appenders.map(&:level)}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Enable logging via LOG env var (LOG=all for all logs, LOG=ar for ActiveRecord only)
|
|
66
|
+
config.around do |ex|
|
|
67
|
+
next ex.call if ENV['LOG'].blank?
|
|
68
|
+
|
|
69
|
+
level = ENV.fetch('LOG_LEVEL', 'trace').to_sym
|
|
70
|
+
ENV['LOG'].casecmp('ar').zero? ? with_ar_logging(level, &ex) : with_logging(level, &ex)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# If TestProf is loaded, also patch its LoggingHelpers for compatibility
|
|
76
|
+
def patch_test_prof!
|
|
77
|
+
return unless defined?(TestProf::Rails::LoggingHelpers)
|
|
78
|
+
|
|
79
|
+
TestProf::Rails::LoggingHelpers.prepend(LoggingHelpers)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require 'semantic_logger'
|
|
2
|
+
|
|
3
|
+
module RailsSemanticLogging
|
|
4
|
+
module RSpec
|
|
5
|
+
# In-memory appender that collects log entries for assertion
|
|
6
|
+
class InMemoryAppender < SemanticLogger::Subscriber
|
|
7
|
+
attr_reader :logs
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
super(level: :trace)
|
|
11
|
+
@logs = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def log(log_entry)
|
|
15
|
+
@logs << log_entry
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def flush
|
|
19
|
+
# No-op for in-memory
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# RSpec matcher for asserting log output from a block.
|
|
24
|
+
#
|
|
25
|
+
# Usage:
|
|
26
|
+
# expect { logger.info("hello") }.to log_semantic(level: :info, message: /hello/)
|
|
27
|
+
# expect { logger.warn("oops", payload: { key: "val" }) }.to log_semantic(payload: { key: "val" })
|
|
28
|
+
class LogSemanticMatcher
|
|
29
|
+
def initialize(expected)
|
|
30
|
+
@expected_level = expected[:level]
|
|
31
|
+
@expected_message = expected[:message]
|
|
32
|
+
@expected_named_tags = expected[:named_tags]
|
|
33
|
+
@expected_payload = expected[:payload]
|
|
34
|
+
@captured_logs = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def matches?(block)
|
|
38
|
+
appender = InMemoryAppender.new
|
|
39
|
+
SemanticLogger.add_appender(appender: appender)
|
|
40
|
+
# Temporarily lower log level to capture all messages
|
|
41
|
+
previous_level = SemanticLogger.default_level
|
|
42
|
+
SemanticLogger.default_level = :trace
|
|
43
|
+
block.call
|
|
44
|
+
SemanticLogger.flush
|
|
45
|
+
SemanticLogger.default_level = previous_level
|
|
46
|
+
SemanticLogger.remove_appender(appender)
|
|
47
|
+
|
|
48
|
+
@captured_logs = appender.logs
|
|
49
|
+
@captured_logs.any? { |log| matches_log?(log) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def supports_block_expectations?
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def failure_message
|
|
57
|
+
"expected block to log a message matching #{expected_description}, but captured logs were:\n#{format_logs}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def failure_message_when_negated
|
|
61
|
+
"expected block not to log a message matching #{expected_description}, but it did"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def matches_log?(log)
|
|
67
|
+
matches_level?(log) && matches_message?(log) && matches_named_tags?(log) && matches_payload?(log)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def matches_level?(log)
|
|
71
|
+
return true unless @expected_level
|
|
72
|
+
|
|
73
|
+
log.level == @expected_level
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def matches_message?(log)
|
|
77
|
+
return true unless @expected_message
|
|
78
|
+
|
|
79
|
+
case @expected_message
|
|
80
|
+
when Regexp then @expected_message.match?(log.message.to_s)
|
|
81
|
+
else log.message.to_s == @expected_message.to_s
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def matches_named_tags?(log)
|
|
86
|
+
return true unless @expected_named_tags
|
|
87
|
+
|
|
88
|
+
@expected_named_tags.all? { |key, value| log.named_tags[key] == value }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def matches_payload?(log)
|
|
92
|
+
return true unless @expected_payload
|
|
93
|
+
return false unless log.payload
|
|
94
|
+
|
|
95
|
+
@expected_payload.all? { |key, value| log.payload[key] == value }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def expected_description
|
|
99
|
+
parts = []
|
|
100
|
+
parts << "level: #{@expected_level.inspect}" if @expected_level
|
|
101
|
+
parts << "message: #{@expected_message.inspect}" if @expected_message
|
|
102
|
+
parts << "named_tags: #{@expected_named_tags.inspect}" if @expected_named_tags
|
|
103
|
+
parts << "payload: #{@expected_payload.inspect}" if @expected_payload
|
|
104
|
+
"{#{parts.join(', ')}}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_logs
|
|
108
|
+
return ' (none)' if @captured_logs.empty?
|
|
109
|
+
|
|
110
|
+
@captured_logs.map { |log| " [#{log.level}] #{log.message} tags=#{log.named_tags} payload=#{log.payload}" }.join("\n")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
module Matchers
|
|
115
|
+
def log_semantic(expected = {})
|
|
116
|
+
LogSemanticMatcher.new(expected)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
require_relative 'rails_semantic_logging/version'
|
|
2
|
+
require_relative 'rails_semantic_logging/configuration'
|
|
3
|
+
require_relative 'rails_semantic_logging/formatters/datadog'
|
|
4
|
+
|
|
5
|
+
module RailsSemanticLogging
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative 'rails_semantic_logging/railtie'
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_semantic_logging
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fabio Napoleoni
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: anyway_config
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rails_semantic_logger
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '4.0'
|
|
54
|
+
description: Provides a consistent, opinionated setup for structured JSON logging
|
|
55
|
+
in Rails, with specific hooks for Sidekiq, ActiveJob, and Puma, as well as Datadog-friendly
|
|
56
|
+
formatters.
|
|
57
|
+
email:
|
|
58
|
+
- f.napoleoni@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- lib/rails_semantic_logging.rb
|
|
66
|
+
- lib/rails_semantic_logging/action_controller/default_payload.rb
|
|
67
|
+
- lib/rails_semantic_logging/configuration.rb
|
|
68
|
+
- lib/rails_semantic_logging/datadog/log_injection.rb
|
|
69
|
+
- lib/rails_semantic_logging/formatters/datadog.rb
|
|
70
|
+
- lib/rails_semantic_logging/job_logging/active_job_patch.rb
|
|
71
|
+
- lib/rails_semantic_logging/job_logging/sidekiq_patch.rb
|
|
72
|
+
- lib/rails_semantic_logging/railtie.rb
|
|
73
|
+
- lib/rails_semantic_logging/rspec/helpers.rb
|
|
74
|
+
- lib/rails_semantic_logging/rspec/matchers.rb
|
|
75
|
+
- lib/rails_semantic_logging/version.rb
|
|
76
|
+
homepage: https://github.com/fabn/rails_semantic_logging
|
|
77
|
+
licenses:
|
|
78
|
+
- MIT
|
|
79
|
+
metadata:
|
|
80
|
+
homepage_uri: https://github.com/fabn/rails_semantic_logging
|
|
81
|
+
source_code_uri: https://github.com/fabn/rails_semantic_logging
|
|
82
|
+
changelog_uri: https://github.com/fabn/rails_semantic_logging/releases
|
|
83
|
+
rubygems_mfa_required: 'true'
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.2'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.6.9
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Opinionated Rails semantic logger configuration with Datadog support
|
|
101
|
+
test_files: []
|