trailer 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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-----