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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.circleci/config.yml +116 -0
- data/.env.example +6 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +263 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +8 -0
- data/README.md +407 -0
- data/Rakefile +8 -0
- data/bin/console +19 -0
- data/bin/setup +8 -0
- data/certs/daveperrett.pem +25 -0
- data/lib/trailer.rb +40 -0
- data/lib/trailer/concern.rb +89 -0
- data/lib/trailer/configuration.rb +46 -0
- data/lib/trailer/middleware/rack.rb +24 -0
- data/lib/trailer/middleware/sidekiq.rb +20 -0
- data/lib/trailer/railtie.rb +19 -0
- data/lib/trailer/recorder.rb +53 -0
- data/lib/trailer/storage/cloud_watch.rb +99 -0
- data/lib/trailer/utility.rb +60 -0
- data/lib/trailer/version.rb +5 -0
- data/trailer.gemspec +47 -0
- metadata +277 -0
- metadata.gz.sig +0 -0
data/README.md
ADDED
@@ -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
|
+

|
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.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
@@ -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-----
|