trailer 0.1.4

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.
@@ -0,0 +1,407 @@
1
+ # Trailer
2
+
3
+ Trailer provides a Ruby framework for tracing events in the context of a request or background job. It allows you to tag and log events with metadata, so that you can search later for e.g. all events and exceptions related to a particular request.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'trailer'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install trailer
20
+
21
+ ## Usage
22
+
23
+ ### Configuration
24
+
25
+ Configure the gem in `config/initializers/trailer.rb`:
26
+
27
+ ```
28
+ Trailer.configure do |config|
29
+ config.application_name = 'shuttlerock'
30
+ config.aws_access_key_id = 'XXXXXXXXXXXXXXXXXXXX'
31
+ config.aws_secret_access_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
32
+ config.service_name = 'auth'
33
+ end
34
+ ```
35
+
36
+ Option | Required? | Default | Description
37
+ ------------------------|-----------------------------------|--------------------------------|-------------|
38
+ `application_name` | Yes | | The global application or company name. This can also be configured with the `TRAILER_APPLICATION_NAME` environment variable. |
39
+ `auto_tag_fields` | | `/(_id\|_at)$/` | When tracing ActiveRecord instances, automatically tag the trace with fields matching this regex. |
40
+ `aws_access_key_id` | Yes (if using CloudWatch storage) | | AWS access key with CloudWatch write permission. |
41
+ `aws_region` | | `'us-east-1'` | The AWS region to log to. |
42
+ `aws_secret_access_key` | Yes (if using CloudWatch storage) | | The AWS secret key. |
43
+ `current_user_method` | | | Allows you provide the name of a method (eg. `:current_user`) that provides a user instance. Trailer will automatically tag the `id` of this user if the option is provided (disabled by default). |
44
+ `enabled` | | `true` | Allows tracing to be conditionally disabled. |
45
+ `environment` | | | The environment that the application is running (eg. `production`, `test`). This can also be configured with the `TRAILER_ENV`, `RAILS_ENV` or `RACK_ENV` environment variables. |
46
+ `host_name` | | | The name of the individual host or server within the service. This can also be configured with the `TRAILER_HOST_NAME` environment variable. |
47
+ `service_name` | Yes | | The name of the service within the application. This can also be configured with the `TRAILER_SERVICE_NAME` environment variable. |
48
+ `storage` | | `Trailer::Storage::CloudWatch` | The storage class to use. |
49
+ `tag_fields` | | `['name']` | When tracing ActiveRecord instances, tag the trace with these fields. |
50
+
51
+ ### Plain Ruby
52
+
53
+ Tracing consists of a `start`, a number of `write`s, and a `finish`:
54
+
55
+ ```
56
+ trail = Trailer.new
57
+ trail.start
58
+ ...
59
+ order = Order.new(state: :open)
60
+ order.save!
61
+ trail.write(order_id: order.id, state: order.state)
62
+ ...
63
+ order.update(state: :closed, price_cents: 1_000)
64
+ trail.write(order_id: order.id, state: order.state, price: order.price_cents)
65
+
66
+ # Finish, and flush data to storage.
67
+ trail.finish
68
+ ```
69
+
70
+ Each call to `start` will create a unique trace ID, that will be persisted with each `write`, allowing you to e.g. search for all events related to a particular HTTP request. Data will not be persisted until `finish` is called. You can `start` and `finish` the same `Trailer` instance multiple times, as long as you `finish` the previous trace before you `start` a new one.
71
+
72
+ ### Rails
73
+
74
+ `Trailer::Middleware::Rack` will be automatically added to Rails for you. `Trailer::Concern` provides three methods to simplify the tracing of objects:
75
+
76
+ - `trace_method`
77
+ - `trace_class`
78
+ - `trace_event`
79
+
80
+ The simplest way to start tracing is to include `Trailer::Concern` and wrap an operation with `trace_method`:
81
+
82
+ ```
83
+ class PagesController < ApplicationController
84
+ include Trailer::Concern
85
+
86
+ def index
87
+ trace_method do
88
+ book = Book.find(params[:id])
89
+ expensive_operation_to_list_pages(book)
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ Every time `index` is requested, Trailer will record that the method was called, and add some metadata:
96
+
97
+ ```
98
+ {
99
+ "event": "PagesController#index",
100
+ "duration": 112,
101
+ "environment": "production",
102
+ "host_name": "web.1",
103
+ "service_name": "studio-api",
104
+ "trace_id": "1-5f465669-97185c244365a889fca9c6fc"
105
+ }
106
+ ```
107
+
108
+ This is not particularly useful by itself - you didn't record anything about the book whose pages you are `index`ing. You can pass the `Book` instance to improve visibility:
109
+
110
+ ```
111
+ def index
112
+ book = Book.find(params[:id])
113
+
114
+ trace_method(book) do
115
+ expensive_operation_to_list_pages(book)
116
+ end
117
+ end
118
+ ```
119
+
120
+ Now every time `index` is requested you'll see `Book` metadata as well, such as the `book_id`, `author_id` and Rails timestamps:
121
+
122
+ ```
123
+ {
124
+ "event": "PagesController#index",
125
+ "book_id": 15,
126
+ "author_id": 12,
127
+ "created_at": "2020-08-26 21:56:12 +0900",
128
+ "updated_at": "2020-08-26 21:57:05 +0900",
129
+ ...
130
+ }
131
+ ```
132
+
133
+ The `auto_tag_fields` and `tag_fields` configuration options are used to decide which fields from the `Book` instance you collect (see [Configuration](#configuration) for more details). The resource provided doesn't have to be an `ActiveRecord` instance - a `Hash` will work as well.
134
+
135
+ If you only want to record the class name rather than the class + method, use the `trace_class` method:
136
+
137
+ ```
138
+ class ArchiveJob
139
+ def perform(book)
140
+ trace_class(book) do
141
+ book.archive!
142
+ end
143
+ end
144
+ end
145
+ ```
146
+
147
+ This will record `"event": "ArchiveJob"` instead of `"event": "ArchiveJob#perform"`. This is useful in situations where the method name doesn't provide any additional information (eg. background jobs always implement `perform`, and GraphQL resolvers implement `resolve`).
148
+
149
+ The `trace_event` method is similar `trace_method` and `trace_class`, but it requires an event name to be passed as the first argument:
150
+
151
+ ```
152
+ class PagesController < ApplicationController
153
+ include Trailer::Concern
154
+
155
+ def index
156
+ book = Book.find(params[:id])
157
+
158
+ @pages = trace_event(:list_pages, book) do
159
+ expensive_operation_to_list_pages(book)
160
+ end
161
+ end
162
+
163
+ def destroy
164
+ page = Page.find(params[:id])
165
+
166
+ trace_event(:destroy_page, page) do
167
+ page.destroy!
168
+ end
169
+
170
+ redirect_to pages_path
171
+ end
172
+ end
173
+ ```
174
+
175
+ You can also provide your own tags to any of the trace methods to augment the automated tags:
176
+
177
+ ```
178
+ trace_event(:destroy_page, page, user: current_user.id, role: user.role) do
179
+ page.destroy!
180
+ end
181
+ ```
182
+
183
+ The concern is not restricted to Rails controllers - it should work with any Ruby class:
184
+
185
+ ```
186
+ class ExpensiveService
187
+ include Trailer::Concern
188
+
189
+ def calculate(record)
190
+ trace_event(:expensive_calculation, record) do
191
+ ...
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+ If you have a method similar to Devise's [current_user](https://github.com/heartcombo/devise#controller-filters-and-helpers), you can automatically augment the trace with the ID of the user performing the action:
198
+
199
+ ```
200
+ # config/initializers/trailer.rb
201
+ Trailer.configure do |config|
202
+ config.current_user_method = :current_user
203
+ end
204
+
205
+ # app/controllers/pages_controller.rb
206
+ class PagesController < ApplicationController
207
+ include Trailer::Concern
208
+
209
+ def index
210
+ book = Book.find(params[:id])
211
+
212
+ trace_method(book) do
213
+ expensive_operation_to_list_pages(book)
214
+ end
215
+ end
216
+
217
+ def current_user
218
+ User.find(session[:user_id])
219
+ end
220
+ end
221
+ ```
222
+
223
+ This will add the `current_user_id` to the trace metadata:
224
+
225
+ ```
226
+ {
227
+ "event": "PagesController#index",
228
+ "current_user_id": 26,
229
+ ...
230
+ }
231
+ ```
232
+
233
+ The middleware will automatically trace exceptions as well:
234
+
235
+ ```
236
+ def index
237
+ book = Book.find(params[:id])
238
+
239
+ trace_method(book) do
240
+ expensive_operation_to_list_pages(book)
241
+ end
242
+
243
+ raise StandardError, 'Something went wrong!'
244
+ end
245
+ ```
246
+
247
+ This will record both the method call and the exception:
248
+
249
+ ```
250
+ {
251
+ "event": "PagesController#index",
252
+ "trace_id": "1-5f465669-97185c244365a889fca9c6fc",
253
+ ...
254
+ }
255
+
256
+ {
257
+ "exception": "StandardError",
258
+ "message": "Something went wrong!",
259
+ "trace_id": "1-5f465669-97185c244365a889fca9c6fc",
260
+ "trace": [...]
261
+ ...
262
+ }
263
+ ```
264
+
265
+ The result of the block is returned, so you can assign a trace to a variable:
266
+
267
+ ```
268
+ record = trace_method(params[:advert]) do
269
+ Advert.create(params[:advert])
270
+ end
271
+ ```
272
+
273
+ Similarly, you can use a trace as the return value of a method:
274
+
275
+ ```
276
+ def add(a, b)
277
+ trace_method { a + b }
278
+ end
279
+ ```
280
+
281
+
282
+ ### No Rails?
283
+
284
+ You can use the Middleware in any rack application. You'll have to add this somewhere:
285
+
286
+ ```
287
+ use Trailer::Middleware::Rack
288
+ ```
289
+
290
+ ### Sidekiq
291
+
292
+ If you are using Sidekiq, `Trailer::Middleware::Sidekiq` will be automatically added to the sidekiq middle chain for you. You can trace operations using the standard `Trailer::Concern` method:
293
+
294
+ ```
295
+ class AuditJob < ApplicationJob
296
+ include Trailer::Concern
297
+
298
+ def perform(user)
299
+ trace_class(user) do
300
+ expensive_operation()
301
+ end
302
+ end
303
+ end
304
+ ```
305
+
306
+ If you're not using Rails, you'll need to add the Sidekiq middleware explicitly:
307
+
308
+ ```
309
+ ::Sidekiq.configure_server do |config|
310
+ config.server_middleware do |chain|
311
+ chain.add Trailer::Middleware::Sidekiq
312
+ end
313
+ end
314
+ ```
315
+
316
+ ## Storage
317
+
318
+ Currently the only provided storage backend is AWS CloudWatch Logs, but you can easily implement your own backend if necessary. New backends should:
319
+
320
+ - Include [Concurrent::Async](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Async.html) from [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) in order to provide non-blocking writes.
321
+ - Implement a `write` method that takes a hash as an argument.
322
+ - Implement a `flush` method that persists the data.
323
+
324
+ ```
325
+ class MyStorage
326
+ include Concurrent::Async
327
+
328
+ def write(data)
329
+ ...
330
+ end
331
+
332
+ def flush
333
+ ...
334
+ end
335
+ end
336
+
337
+ Trailer.configure do |config|
338
+ config.storage = MyStorage
339
+ end
340
+ ```
341
+
342
+ ## CloudWatch Permissions
343
+
344
+ The AWS account needs the following CloudWatch Logs permissions:
345
+
346
+ ```
347
+ {
348
+ "Version": "2012-10-17",
349
+ "Statement": [
350
+ {
351
+ "Sid": "VisualEditor0",
352
+ "Effect": "Allow",
353
+ "Action": [
354
+ "logs:CreateLogStream",
355
+ "logs:DescribeLogGroups",
356
+ "logs:DescribeLogStreams",
357
+ "logs:CreateLogGroup",
358
+ "logs:PutLogEvents"
359
+ ],
360
+ "Resource": [
361
+ "arn:aws:logs:us-east-1:XXXXXXXXXXXX:log-group:my-log-group-name",
362
+ "arn:aws:logs:us-east-1:XXXXXXXXXXXX:log-group:my-log-group-name:log-stream:my-log-stream-name"
363
+ ]
364
+ }
365
+ ]
366
+ }
367
+ ```
368
+
369
+ The ARNs in the `Resource` section are for demonstration purposes only - substitute your own, or use `"Resource": "*"` to allow global access.
370
+
371
+ ## Searching for traces in AWS CloudWatch
372
+
373
+ CloudWatch allows you to search for specific attributes:
374
+
375
+ - Search for a specific `order_id`: `{ $.order_id = "aaa" }`
376
+ - Search for all records from a particular request or job: `{ $.trace_id = "1-5f44617e-6bcd7259689e5d303d4ad430" }`)
377
+ - Search for multiple attributes: `{ $.order_id = "order-aaa" && $.duration = 1 }`
378
+ - Search for one of several attributes: `{ $.order_id = "aaa" || $.order_id = "bbb" }`
379
+ - Search for a specific user: `{ $.current_user_id = 1234 }`
380
+ - Search for all records containing a particular attribute, regardless of its value: `{ $.duration = * }`
381
+
382
+ Trailer provides some standard attributes that might be useful:
383
+
384
+ Attribute | Description
385
+ ---------------|-------------|
386
+ `duration` | The duration of the trace in milliseconds.
387
+ `host_name` | The (optional) host name specified during `Trailer.configure`.
388
+ `service_name` | The service name specified during `Trailer.configure`.
389
+ `trace_id` | A unique ID identifying all records from a single request or Sidekiq job. This allows you to track all events within the context of a single request.
390
+
391
+ You can also filter by partial wildcard, search nested objects, and much more - see [Filter and Pattern Syntax](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html) for more information.
392
+
393
+ ![Searching CloudWatch](https://static.shuttlerock-cdn.com/staff/dave/trailer-gem/CloudWatch_Screenshot.png)
394
+
395
+ ## Todo
396
+
397
+ - Allow the trace ID to be set manually, in case we want to trace distributed systems.
398
+
399
+ ## Development
400
+
401
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
402
+
403
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
404
+
405
+ ## Contributing
406
+
407
+ Bug reports and pull requests are welcome on GitHub at https://github.com/shuttlerock/trailer.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'trailer'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ begin
14
+ require 'dotenv/load'
15
+ rescue LoadError; end
16
+ Trailer.configure
17
+
18
+ require 'irb'
19
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIERDCCAqygAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtoZWxs
3
+ by9EQz1kYXZlcGVycmV0dC9EQz1jb20wHhcNMjAwODI1MDIwMjIxWhcNMjEwODI1
4
+ MDIwMjIxWjAmMSQwIgYDVQQDDBtoZWxsby9EQz1kYXZlcGVycmV0dC9EQz1jb20w
5
+ ggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDEoZcWt+lZXpi08uiOAyDX
6
+ nV8Ujd1oi+z3k0fdTqZHT+9sTf4Uj24aELpACbpwX4613l8ysrbAy4DTB6GZt3Ue
7
+ mCXWcwLeelmYIQ6TONsLQ25nyOoinLiZ2VRLGdt+gHNhY7PkZD1BZWfIZ1R1wk2r
8
+ MxlIQ1Ohrig6Ok3r3CA8h/yG1Rhb7tX+4s36qBlpxvFcDcffU+9o/kdqbo/dbGC+
9
+ jXv3f/NJ9nZNs1WuuS7VcgFrdJdVHAM4qyyZZG7KRN+6RI0kYXiT8KDA/meK07DX
10
+ IMrOrcXtz30M9QaUP0iu1S5A0OXZM68hVfnaBDMJf5XdiJQWkrVEWTk9qJD69SVn
11
+ xHs2yI4tejry4haoZ5Gtw4xS/M00EJo650ENdpbH9IW97ytGEGv60+SIvMeImBL9
12
+ rP8luh07kjbvcxa2O64LqABGNSvYYXumGTK/CpRx229PJoelO5eqPV4b5A4VYOpI
13
+ NemkWSYiJlQQms2jy++phKiHoIchmrtJBOKEwND1svcCAwEAAaN9MHswCQYDVR0T
14
+ BAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFPO/KylzyTq3FnQbKh7ilMoDfDFY
15
+ MCAGA1UdEQQZMBeBFWhlbGxvQGRhdmVwZXJyZXR0LmNvbTAgBgNVHRIEGTAXgRVo
16
+ ZWxsb0BkYXZlcGVycmV0dC5jb20wDQYJKoZIhvcNAQELBQADggGBAI3TepwbjOiP
17
+ WiVf1P+mWktyuBFgPt2rAU+/dgT4q6UBGAsQLmzVe+Na5jx0XLRw29P6xODGpZKY
18
+ I/hluXuKyousxrGF7ObVdAMLpJBBhUf7ItXxNvmxE1OXhiThzKNu3SUBD9E4S+An
19
+ u1HMQW/F307IxNQdjSM/M0G6KDhbwjy99biysmbCMRzQNJNv/fZshhIfgaAegrnG
20
+ Rdrwjlth7RvkIs4YLZyr0x0OlvfN0qHnAlKZA8ZAtt11hA0ooSHuRUcgYL7Fwzlk
21
+ VQEIGqqdRT88oMOkP4gJXOZA0VQ9BsKa0FWqm+uSXJQURp5up9zL9I61kbI5HbqO
22
+ L6psY9/rXNft9LABlmYpgAjV85LM5g53E6QVZqeXANljpJSvsFiVh1W+4YGGAleo
23
+ 3Um/SScB6alBpv/eTa/qPXa+S84pZDU6kFDasnH7GEl6jYlVxZbzpHUIvkDIOp5J
24
+ 6O1XOcLc39AfX5etu1WZ+wx3xUzux0oqS6rXcl63HmuKe8Son7RqJQ==
25
+ -----END CERTIFICATE-----