trailer 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![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.
|
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-----
|