httpigeon 2.0.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/.github/PULL_REQUEST_TEMPLATE/new_feature_template.md +7 -0
- data/.github/workflows/ci.yaml +29 -0
- data/.github/workflows/publish.yaml +45 -0
- data/.github/workflows/release-please.yaml +26 -0
- data/.github/workflows/reviewdog.yaml +19 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +187 -0
- data/CHANGELOG.md +48 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +191 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/httpigeon.gemspec +42 -0
- data/lib/httpigeon/configuration.rb +16 -0
- data/lib/httpigeon/log_redactor.rb +96 -0
- data/lib/httpigeon/logger.rb +84 -0
- data/lib/httpigeon/middleware/httpigeon_logger.rb +37 -0
- data/lib/httpigeon/request.rb +96 -0
- data/lib/httpigeon/version.rb +3 -0
- data/lib/httpigeon.rb +43 -0
- metadata +180 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e9a75a860ebdb91a4efcfe5d69f7e7a04da7b6f1762b2a67fb1d9ec5aefe2cab
|
|
4
|
+
data.tar.gz: 47f3ac2004f2d716888b36febc71e411ae5afdc8d62bc1a47b536c8fa100a1e8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e8f8f90f1aa68d59f67e4c3fe51932450755c291092ba7e9baa0e8f17502d09c7adc6186c9f1c229a5b54470192dd6b0c0e4d8e4e6c69dc1dab4d051b923dfbc
|
|
7
|
+
data.tar.gz: dfd927e24af3dfc5c08d9c7e6a7a9767e5f9207b87286143b580c33f36525f24f87a9656dc133b84807f84001f9381ab380ed4dc6286444b40c1ab5f664ff59d
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, reopened, edited, synchronize]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
validate-conventional-commit:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Check for conventional commit
|
|
12
|
+
uses: agenthunt/conventional-commit-checker-action@v1.0.0
|
|
13
|
+
with:
|
|
14
|
+
pr-title-regex: '^((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.*\))?(!)?(: (.*\s*)*))|(^Merge (.*\s*)*)|(^Initial commit$)'
|
|
15
|
+
pr-body-regex: '.*'
|
|
16
|
+
|
|
17
|
+
unit-test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
if: ${{ github.event.action != 'edited' }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v3
|
|
22
|
+
- name: Set up Ruby
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: 3.1
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: bundle install
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: bundle exec rake spec
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
workflow_call:
|
|
4
|
+
inputs:
|
|
5
|
+
public_publish:
|
|
6
|
+
required: false
|
|
7
|
+
type: string
|
|
8
|
+
default: 'false'
|
|
9
|
+
private_publish:
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
default: 'false'
|
|
13
|
+
jobs:
|
|
14
|
+
release:
|
|
15
|
+
name: Release gem
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Check out code
|
|
19
|
+
uses: actions/checkout@v3
|
|
20
|
+
- name: Setup Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: 3.1
|
|
24
|
+
- name: Publish to GitHub
|
|
25
|
+
if: inputs.private_publish == 'true'
|
|
26
|
+
run: |
|
|
27
|
+
mkdir -p $HOME/.gem
|
|
28
|
+
touch $HOME/.gem/credentials
|
|
29
|
+
chmod 0600 $HOME/.gem/credentials
|
|
30
|
+
printf -- "---\n:github: Bearer ${GITHUB_TOKEN}\n" > $HOME/.gem/credentials
|
|
31
|
+
gem build *.gemspec
|
|
32
|
+
gem push --KEY github --host https://rubygems.pkg.github.com/dailypay *.gem
|
|
33
|
+
env:
|
|
34
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
- name: Publish to RubyGems
|
|
36
|
+
if: inputs.public_publish == 'true'
|
|
37
|
+
run: |
|
|
38
|
+
mkdir -p $HOME/.gem
|
|
39
|
+
touch $HOME/.gem/credentials
|
|
40
|
+
chmod 0600 $HOME/.gem/credentials
|
|
41
|
+
printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
|
|
42
|
+
gem build *.gemspec
|
|
43
|
+
gem push *.gem
|
|
44
|
+
env:
|
|
45
|
+
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: release-please
|
|
2
|
+
on: [pull_request]
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
release-please:
|
|
6
|
+
outputs:
|
|
7
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
8
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
9
|
+
permissions: write-all
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: google-github-actions/release-please-action@v3
|
|
13
|
+
id: release
|
|
14
|
+
with:
|
|
15
|
+
release-type: ruby
|
|
16
|
+
package-name: httpigeon
|
|
17
|
+
bump-minor-pre-major: true
|
|
18
|
+
bump-patch-for-minor-pre-major: true
|
|
19
|
+
publish-gem:
|
|
20
|
+
uses: ./.github/workflows/publish.yaml
|
|
21
|
+
needs: [ release-please ]
|
|
22
|
+
# if: needs.release-please.outputs.release_created
|
|
23
|
+
with:
|
|
24
|
+
public_publish: 'true'
|
|
25
|
+
private_publish: 'false'
|
|
26
|
+
secrets: inherit # implicitly forward secrets to called workflow
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Reviewdog
|
|
2
|
+
on: [pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
rubocop:
|
|
5
|
+
name: rubocop
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
steps:
|
|
8
|
+
- name: Check out code
|
|
9
|
+
uses: actions/checkout@v3
|
|
10
|
+
- uses: ruby/setup-ruby@v1
|
|
11
|
+
with:
|
|
12
|
+
ruby-version: 3.1
|
|
13
|
+
- name: rubocop
|
|
14
|
+
uses: reviewdog/action-rubocop@v2
|
|
15
|
+
with:
|
|
16
|
+
rubocop_version: gemfile
|
|
17
|
+
rubocop_extensions: rubocop-rspec:gemfile
|
|
18
|
+
reporter: github-pr-check
|
|
19
|
+
fail_on_error: true
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
TargetRubyVersion: 3.1.2
|
|
6
|
+
Exclude:
|
|
7
|
+
- 'bin/**/*'
|
|
8
|
+
- 'vendor/**/*'
|
|
9
|
+
NewCops: enable
|
|
10
|
+
|
|
11
|
+
#### ::STYLE ####
|
|
12
|
+
|
|
13
|
+
# Let Ruby do it's thing when the time comes
|
|
14
|
+
Style/FrozenStringLiteralComment:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
# Decision whether to use alias or alias_method is not stylistic
|
|
18
|
+
# See: https://blog.bigbinary.com/2012/01/08/alias-vs-alias-method.html
|
|
19
|
+
Style/Alias:
|
|
20
|
+
Enabled: false
|
|
21
|
+
|
|
22
|
+
# This still common in Rails and usually doesn't result in problems.
|
|
23
|
+
Style/ClassAndModuleChildren:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Style/CollectionMethods:
|
|
27
|
+
Description: Preferred collection methods.
|
|
28
|
+
StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size
|
|
29
|
+
Enabled: true
|
|
30
|
+
PreferredMethods:
|
|
31
|
+
collect: map
|
|
32
|
+
collect!: map!
|
|
33
|
+
find: detect
|
|
34
|
+
find_all: select
|
|
35
|
+
reduce: inject
|
|
36
|
+
|
|
37
|
+
# We don't enforce per-class documentation.
|
|
38
|
+
Style/Documentation:
|
|
39
|
+
Enabled: false
|
|
40
|
+
|
|
41
|
+
# We don't mind two-line empty methods as they're easier to start editing and
|
|
42
|
+
# pretty common in auto-generated Rails controllers.
|
|
43
|
+
Style/EmptyMethod:
|
|
44
|
+
Enabled: false
|
|
45
|
+
|
|
46
|
+
# We allow hash rockets in rake task dependencies, e.g. task :my_task => :dep.
|
|
47
|
+
Style/HashSyntax:
|
|
48
|
+
EnforcedShorthandSyntax: never
|
|
49
|
+
|
|
50
|
+
# There's no statistical difference between single and double quotes
|
|
51
|
+
# performance.
|
|
52
|
+
# See: https://www.viget.com/articles/just-use-double-quoted-ruby-strings/
|
|
53
|
+
Style/StringLiterals:
|
|
54
|
+
Enabled: false
|
|
55
|
+
|
|
56
|
+
# Ditto for above.
|
|
57
|
+
Style/StringLiteralsInInterpolation:
|
|
58
|
+
Enabled: false
|
|
59
|
+
|
|
60
|
+
Style/BlockDelimiters:
|
|
61
|
+
AllowBracesOnProceduralOneLiners: true
|
|
62
|
+
|
|
63
|
+
Style/ModuleFunction:
|
|
64
|
+
EnforcedStyle: extend_self
|
|
65
|
+
|
|
66
|
+
Style/SymbolArray:
|
|
67
|
+
MinSize: 3
|
|
68
|
+
|
|
69
|
+
Style/WordArray:
|
|
70
|
+
MinSize: 3
|
|
71
|
+
|
|
72
|
+
Style/FetchEnvVar:
|
|
73
|
+
Enabled: false
|
|
74
|
+
|
|
75
|
+
Style/OpenStructUse:
|
|
76
|
+
Enabled: false
|
|
77
|
+
|
|
78
|
+
Style/RedundantInitialize:
|
|
79
|
+
Exclude:
|
|
80
|
+
- 'test/**/*.rb'
|
|
81
|
+
|
|
82
|
+
Style/Semicolon:
|
|
83
|
+
Exclude:
|
|
84
|
+
- 'test/**/*.rb'
|
|
85
|
+
|
|
86
|
+
Style/Lambda:
|
|
87
|
+
Enabled: false
|
|
88
|
+
|
|
89
|
+
#### ::LAYOUT ####
|
|
90
|
+
|
|
91
|
+
Layout/DotPosition:
|
|
92
|
+
Enabled: true
|
|
93
|
+
EnforcedStyle: trailing
|
|
94
|
+
|
|
95
|
+
Layout/MultilineMethodCallIndentation:
|
|
96
|
+
EnforcedStyle: indented_relative_to_receiver
|
|
97
|
+
Enabled: false
|
|
98
|
+
IndentationWidth: ~
|
|
99
|
+
|
|
100
|
+
# This rule does not detect string interpolations reliably,
|
|
101
|
+
# e.g. accuses 'full_messages.join(", ")'
|
|
102
|
+
Layout/SpaceInsideStringInterpolation:
|
|
103
|
+
Enabled: false
|
|
104
|
+
|
|
105
|
+
# Allow long lines with comments
|
|
106
|
+
Layout/LineLength:
|
|
107
|
+
Max: 200
|
|
108
|
+
AllowedPatterns: ['(\A|\s)#']
|
|
109
|
+
|
|
110
|
+
#### ::LINT ####
|
|
111
|
+
Lint/AssignmentInCondition:
|
|
112
|
+
AllowSafeAssignment: false
|
|
113
|
+
|
|
114
|
+
Lint/ConstantDefinitionInBlock:
|
|
115
|
+
Exclude:
|
|
116
|
+
- 'test/**/*.rb'
|
|
117
|
+
|
|
118
|
+
#### ::METRICS ####
|
|
119
|
+
|
|
120
|
+
# Methods should be easy to read, enforcing an arbitrary metric as number
|
|
121
|
+
# of lines is not the way to do it though.
|
|
122
|
+
Metrics/MethodLength:
|
|
123
|
+
Enabled: false
|
|
124
|
+
|
|
125
|
+
Metrics/ClassLength:
|
|
126
|
+
Enabled: false
|
|
127
|
+
|
|
128
|
+
Metrics/AbcSize:
|
|
129
|
+
Enabled: false
|
|
130
|
+
|
|
131
|
+
Metrics/BlockLength:
|
|
132
|
+
Enabled: false
|
|
133
|
+
|
|
134
|
+
Metrics/CyclomaticComplexity:
|
|
135
|
+
Enabled: false
|
|
136
|
+
|
|
137
|
+
Metrics/PerceivedComplexity:
|
|
138
|
+
Enabled: false
|
|
139
|
+
|
|
140
|
+
Metrics/ParameterLists:
|
|
141
|
+
Enabled: false
|
|
142
|
+
|
|
143
|
+
#### ::NAMING ####
|
|
144
|
+
|
|
145
|
+
# Allow arbitrary symbol names
|
|
146
|
+
Naming/VariableNumber:
|
|
147
|
+
CheckSymbols: false
|
|
148
|
+
|
|
149
|
+
Naming/BlockForwarding:
|
|
150
|
+
Enabled: false
|
|
151
|
+
|
|
152
|
+
#### ::GEM ####
|
|
153
|
+
Gemspec/DevelopmentDependencies:
|
|
154
|
+
Enabled: false
|
|
155
|
+
|
|
156
|
+
Gemspec/OrderedDependencies:
|
|
157
|
+
Enabled: false
|
|
158
|
+
|
|
159
|
+
Gemspec/RequireMFA:
|
|
160
|
+
Enabled: false
|
|
161
|
+
|
|
162
|
+
#### ::RSPEC ####
|
|
163
|
+
RSpec/ExampleLength:
|
|
164
|
+
Enabled: false
|
|
165
|
+
|
|
166
|
+
RSpec/MultipleExpectations:
|
|
167
|
+
Max: 10
|
|
168
|
+
|
|
169
|
+
RSpec/AnyInstance:
|
|
170
|
+
Enabled: false
|
|
171
|
+
|
|
172
|
+
RSpec/VerifiedDoubles:
|
|
173
|
+
Enabled: false
|
|
174
|
+
|
|
175
|
+
RSpec/MultipleMemoizedHelpers:
|
|
176
|
+
Max: 10
|
|
177
|
+
|
|
178
|
+
RSpec/NestedGroups:
|
|
179
|
+
Max: 5
|
|
180
|
+
|
|
181
|
+
RSpec/FilePath:
|
|
182
|
+
Exclude:
|
|
183
|
+
- spec/httpigeon/**/*.rb
|
|
184
|
+
|
|
185
|
+
RSpec/SpecFilePathFormat:
|
|
186
|
+
Exclude:
|
|
187
|
+
- spec/httpigeon/**/*.rb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [2.0.0](https://github.com/dailypay/httpigeon/compare/v1.3.0...v2.0.0) (2023-10-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **logging:** Improve redaction mechanism ([#23](https://github.com/dailypay/httpigeon/issues/23))
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* Generate request ID header by default ([#25](https://github.com/dailypay/httpigeon/issues/25)) ([c2aa078](https://github.com/dailypay/httpigeon/commit/c2aa078947c422f544ff1b36d77576a2a3681d08))
|
|
13
|
+
* **logging:** Improve redaction mechanism ([#23](https://github.com/dailypay/httpigeon/issues/23)) ([ce090bd](https://github.com/dailypay/httpigeon/commit/ce090bd0124ef3f3ec616d7c0af5a4652be11b0a))
|
|
14
|
+
|
|
15
|
+
## [1.3.0](https://github.com/dailypay/httpigeon/compare/v1.2.1...v1.3.0) (2023-08-24)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **request:** Add helper :get and :post class methods ([#21](https://github.com/dailypay/httpigeon/issues/21)) ([e7427a6](https://github.com/dailypay/httpigeon/commit/e7427a6f1fe2d39e4cce2ec3ea1188e03b563287))
|
|
21
|
+
|
|
22
|
+
## [1.2.1](https://github.com/dailypay/httpigeon/compare/v1.2.0...v1.2.1) (2023-07-28)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **logger:** Fix reference to ruby Logger constant ([#19](https://github.com/dailypay/httpigeon/issues/19)) ([ec99bd5](https://github.com/dailypay/httpigeon/commit/ec99bd5b6371256ded6c88c8413b0bd2c926a7a1))
|
|
28
|
+
|
|
29
|
+
## [1.2.0](https://github.com/dailypay/httpigeon/compare/v1.1.1...v1.2.0) (2023-07-28)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
* Add customizable log redactor that can handle both Hash and String payload. Also add support for auto-generating request IDs ([#14](https://github.com/dailypay/httpigeon/issues/14)) ([c3efa0a](https://github.com/dailypay/httpigeon/commit/c3efa0a510cda687f6a6822e17c1c9600ba4dfd0))
|
|
35
|
+
|
|
36
|
+
## [1.1.1](https://github.com/dailypay/httpigeon/compare/v1.1.0...v1.1.1) (2023-06-20)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
* **httpigeon:** Leave event logger constructor signature up to call site ([#10](https://github.com/dailypay/httpigeon/issues/10)) ([03ba441](https://github.com/dailypay/httpigeon/commit/03ba441c66d8ea6562f218b41cc8f724bd98a4a9))
|
|
42
|
+
|
|
43
|
+
## 1.0.0 (2023-06-20)
|
|
44
|
+
Initial release
|
|
45
|
+
|
|
46
|
+
### Features
|
|
47
|
+
|
|
48
|
+
* **httpigeon:** [XAPI-1353] Gemify HTTPigeon library ([#1](https://github.com/dailypay/httpigeon/issues/1)) ([ee89810](https://github.com/dailypay/httpigeon/commit/ee898102b2dffe6623e57a0d799a8b9a37d068a1))
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 DailyPay
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# HTTPigeon
|
|
2
|
+
|
|
3
|
+
As early as 2000 years ago, and as late as 20 years ago, messenger pigeons (a.k.a homing pigeons) were an established and reliable means of long distance communication. This library is dedicated to messenger pigeons and all their contributions towards ancient and modern civilization ❤️.
|
|
4
|
+
|
|
5
|
+
The goal of this library is to add a layer of abstraction on top of [Faraday](https://github.com/lostisland/faraday) client with a much simpler, but also customizable interface, so that making and logging API requests is much easier.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
**Configuration:**
|
|
10
|
+
```ruby
|
|
11
|
+
HTTPigeon.configure do |config|
|
|
12
|
+
config.default_event_type = # Set a default event type for all requests, overridable per request. Default: 'http.outbound'
|
|
13
|
+
config.default_filter_keys = # Set a default list of keys to be redacted for Hash payloads, overridable per request. Default: []
|
|
14
|
+
config.redactor_string = # Set a string that should be used as the replacement when redacting sensitive data. Default: '[FILTERED]'
|
|
15
|
+
config.log_redactor = # Specify an object to be used for redacting data before logging. Must respond to #redact(data<Hash, String>). Default: nil
|
|
16
|
+
config.event_logger = # Specify an object to be used for logging request roundtrip events. Default: $stdout
|
|
17
|
+
config.auto_generate_request_id = # Auto-generate a uuid for each request and store in a 'X-Request-Id' header? Default: true
|
|
18
|
+
config.exception_notifier = # Specify an object to be used for reporting errors. Must respond to #notify_exception(e<Exception>). Must be defined if :notify_all_exceptions is true
|
|
19
|
+
config.notify_all_exceptions = # Do you want these errors to actually get reported/notified? Default: false
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Instantiating with a block:**
|
|
24
|
+
```ruby
|
|
25
|
+
# @option [String] base_url the base URI (required)
|
|
26
|
+
request = HTTPigeon::Request.new(base_url: 'https://dummyjson.com') do |connection|
|
|
27
|
+
# connection is an instance of Faraday::Connection
|
|
28
|
+
connection.headers['foo'] = 'barzzz'
|
|
29
|
+
connection.options['timeout'] = 15
|
|
30
|
+
...
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @option [Symbol] method the HTTP verb (default: :get)
|
|
34
|
+
# @option [String] path the request path (default: '/')
|
|
35
|
+
# @option [Hash] payload the body (for writes) or query params (for reads) of the request (default: {})
|
|
36
|
+
request.run(path: '/users/1')
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Instantiating with customizable arguments:**
|
|
40
|
+
```ruby
|
|
41
|
+
# @param base_url [String] the base URI
|
|
42
|
+
# @param options [Hash] the Faraday connection options (default: {})
|
|
43
|
+
# @param headers [Hash] the request headers (default: {})
|
|
44
|
+
# @param adapter [Faraday::Adapter] the Faraday adapter (default: Net::HTTP)
|
|
45
|
+
# @param logger [Logger] for logging request and response (default: HTTPigeon::Logger)
|
|
46
|
+
# @param event_type [String] for filtering/scoping the logs (default: 'http.outbound')
|
|
47
|
+
# @param log_filters [Array<Symbol, String>] specifies keys in URL, headers and body to be redacted before logging.
|
|
48
|
+
# Can define keys for both Hash and String payloads (default: [])
|
|
49
|
+
request = HTTPigeon::Request.new(base_url: 'https://dummyjson.com', headers: { Accept: 'application/json' }, log_filters: [:api_key, 'access_token', '(client_id=)(\w+)'])
|
|
50
|
+
request.run(path: '/users/1')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Passing a custom logger:**
|
|
54
|
+
|
|
55
|
+
Your custom logger must respond to `#log` and be a registered Faraday Middleware. It can optionally implement `#on_request_start` and `#on_request_finish`, if you wanted to take certain actions right before and right after a request round-trip (e.g capturing latency).
|
|
56
|
+
Note that if you pass a custom logger, you would have to handle redacting sensitive keys even if you pass a `log_filters` list, unless you subclass `HTTPigeon::Logger`.
|
|
57
|
+
```ruby
|
|
58
|
+
# The default Rails logger is registered/recognized by Faraday
|
|
59
|
+
class CustomLogger < Logger
|
|
60
|
+
# @param [Faraday::Env] env the Faraday environment instance passed from middleware
|
|
61
|
+
# @param [Hash] data additional data passed from middleware. May contain an :error object if the request was unsuccessful (default: {})
|
|
62
|
+
def log(env, data)
|
|
63
|
+
error = data.delete(:error).to_json
|
|
64
|
+
|
|
65
|
+
log_data = data.merge(
|
|
66
|
+
{
|
|
67
|
+
method: env.method,
|
|
68
|
+
headers: env.request_headers,
|
|
69
|
+
body: env.response_body,
|
|
70
|
+
error: error,
|
|
71
|
+
latency: @end_time - @start_time
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
super(:info, log_data.to_json)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# optional method
|
|
79
|
+
def on_request_start
|
|
80
|
+
@start_time = Time.current
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# optional method
|
|
84
|
+
def on_request_finish
|
|
85
|
+
@end_time = Time.current
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
request = HTTPigeon::Request.new(base_url: 'https://dummyjson.com', logger: CustomLogger.new)
|
|
90
|
+
request.run(path: '/users/1')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Using the default logger:**
|
|
94
|
+
|
|
95
|
+
***Event Type***
|
|
96
|
+
|
|
97
|
+
The default logger always adds an `:event_type` key to the log payload that can be used as another filtering/grouping mechanism when querying logs. The default value is 'http.outbound'. To set a different value for a specific request, simply pass the key like so:
|
|
98
|
+
```ruby
|
|
99
|
+
HTTPigeon::Request.new(base_url: 'https://dummyjson.com', event_type: 'custom.event')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
***Log Filters***
|
|
103
|
+
|
|
104
|
+
> [!IMPORTANT]
|
|
105
|
+
> - Log filtering mechanism does partial redaction by default, unless the value is **4 characters or less**. To have a value fully redacted, you have to explicitly append a replacement to the filter, separated by a `::` (e.g `'ssn::[REDACTED]'`).
|
|
106
|
+
> - Hash log filters are case insensitive
|
|
107
|
+
> - Only ignore case regexp flag (`/i`) is currently supported for log filters and is already applied by default
|
|
108
|
+
|
|
109
|
+
Prior to logging, the default logger will always run it's redactor through:
|
|
110
|
+
- The full request **URL**
|
|
111
|
+
- The request and response **headers**
|
|
112
|
+
- the request and response **body**
|
|
113
|
+
|
|
114
|
+
**Examples:**
|
|
115
|
+
|
|
116
|
+
Examples assume you set `:redactor_string` in your initializer to `[REDACTED]`
|
|
117
|
+
|
|
118
|
+
| Filter | Target | Pre-redaction | Post-redaction | Notes |
|
|
119
|
+
| --- | --- | --- | --- | ----- |
|
|
120
|
+
| `"email"` OR `:email` | Hash | `{ "email": "atuny0@sohu.com" }` | `{ "email": "atu...[REDACTED]" }` | Filters will get applied to nested objects as well. There's no limit on depth |
|
|
121
|
+
| `"email::[REDACTED]"` | Hash | `{ "email": "atuny0@sohu.com" }` | `{ "email": "[REDACTED]" }` | Replacement can be whatever you want and is applied as-is |
|
|
122
|
+
| `"/email/"` | Hash | `{ "email": "atuny0@sohu.com" }` | `{ "email": "atuny0@sohu.com" }` | Regex filters will not get applied to hash keys. This is a design decision to prevent bugs |
|
|
123
|
+
| `"/(email=)(.*\\.[a-z]+)(&\|$)/"` | String | `https://dummyjson.com/users?email=atuny0@sohu.com` | `https://dummyjson.com/users?email=atu...[REDACTED]` | Regex filters must be in proper regex format but wrapped in a string. If no replacement is specified, [regex grouping](https://learn.microsoft.com/en-us/dotnet/standard/base-types/grouping-constructs-in-regular-expressions) MUST be used |
|
|
124
|
+
| `"/email=.*\\.[a-z]+(&\|$)/::email=[REDACTED]"` | String | `https://dummyjson.com/users?email=atuny0@sohu.com` | `https://dummyjson.com/users?email=[REDACTED]` | Replacement can be whatever you want and is applied as-is. No need to use regex grouping when explicitly specifying a replacement |
|
|
125
|
+
| `"(email=)(.*\\.[a-z]+)(&\|$)"` OR `"email"` | String | `https://dummyjson.com/users?email=atuny0@sohu.com` | `https://dummyjson.com/users?email=atuny0@sohu.com` | String filters must be defined in proper regex format, otherwise they will be ignored. This is a design descision to prevent bugs |
|
|
126
|
+
|
|
127
|
+
There are some ready-made, tokenized filter patterns available that you can take advantage of for **URLs** and/or **URI encoded requests**:
|
|
128
|
+
- HTTPigeon::FilterPatterns::EMAIL
|
|
129
|
+
- HTTPigeon::FilterPatterns::PASSWORD
|
|
130
|
+
- HTTPigeon::FilterPatterns::USERNAME
|
|
131
|
+
- HTTPigeon::FilterPatterns::CLIENT_ID
|
|
132
|
+
- HTTPigeon::FilterPatterns::CLIENT_SECRET
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# Will truncate the value of any header or payload key matching access_token
|
|
136
|
+
# Will replace the value of any header or payload key matching password with [REDACTED]
|
|
137
|
+
# Will truncate the value of any request param URI encoded payload key matching client_id
|
|
138
|
+
# Will replace the value of any request param URI encoded payload key matching password with [REDACTED]
|
|
139
|
+
HTTPigeon::Request.new(base_url: 'https://dummyjson.com', log_filters: %w[access_token password::[REDACTED] /(client_id=)([0-9a-z]+)*/ /password=\w+/::password=[REDACTED]])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Running a request:**
|
|
143
|
+
|
|
144
|
+
* You can pass a block to further customize a specific request:
|
|
145
|
+
```ruby
|
|
146
|
+
request = HTTPigeon::Request.new(base_url: 'https://dummyjson.com')
|
|
147
|
+
|
|
148
|
+
# Returns a Hash (parsed JSON) response or a String if the original response was not valid JSON
|
|
149
|
+
request.run(path: '/users/1') { |request| request.headers['X-Request-Signature'] = Base64.encode64("#{method}::#{path}") }
|
|
150
|
+
|
|
151
|
+
# Access the raw Faraday response
|
|
152
|
+
request.response
|
|
153
|
+
|
|
154
|
+
# Quickly get the response status
|
|
155
|
+
request.response_status
|
|
156
|
+
|
|
157
|
+
# Quickly get the raw response body
|
|
158
|
+
request.response_body
|
|
159
|
+
```
|
|
160
|
+
* There is a convenient :get and :post class method you can use
|
|
161
|
+
```ruby
|
|
162
|
+
# @param endpoint [String] the URI endpoint you're trying to hit
|
|
163
|
+
# @param query [Hash] the request query params (default: {})
|
|
164
|
+
# @param headers [Hash] the request headers (default: {})
|
|
165
|
+
# @param event_type [String] the event type for logs grouping (default: 'http.outbound')
|
|
166
|
+
# @param log_filters [Array<Symbol, String>] specifies keys in URL, headers and body to be redacted before logging.
|
|
167
|
+
# @return [HTTPigeon::Response] an object with attributes :request [HTTPigeon::Request], :parsed_response [Hash], and :raw_response [Faraday::Response]
|
|
168
|
+
response = HTTPigeon::Request.get(endpoint, query, headers, event_type, log_filters)
|
|
169
|
+
|
|
170
|
+
# @param endpoint [String] the URI endpoint you're trying to hit
|
|
171
|
+
# @param payload [Hash] the request payload/body (default: {})
|
|
172
|
+
# @param headers [Hash] the request headers (default: {})
|
|
173
|
+
# @param event_type [String] the event type for logs grouping (default: 'http.outbound')
|
|
174
|
+
# @param log_filters [Array<Symbol, String>] specifies keys in URL, headers and body to be redacted before logging.
|
|
175
|
+
# @return [HTTPigeon::Response] an object with attributes :request [HTTPigeon::Request], :parsed_response [Hash], and :raw_response [Faraday::Response]
|
|
176
|
+
response = HTTPigeon::Request.post(endpoint, payload, headers, event_type, log_filters)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
182
|
+
|
|
183
|
+
To install this gem onto your local machine, run `bundle exec rake install:local`.
|
|
184
|
+
|
|
185
|
+
## Contributing
|
|
186
|
+
|
|
187
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dailypay/httpigeon.
|
|
188
|
+
|
|
189
|
+
**Making Pull Requests:**
|
|
190
|
+
|
|
191
|
+
This project uses [release-please](https://github.com/google-github-actions/release-please-action) for automated releases. As such, any pull request that fails the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary) validation will not be merged.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
7
|
+
task.pattern = Dir.glob("spec/**/*_spec.rb")
|
|
8
|
+
task.rspec_opts = "--format documentation"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require "rubocop/rake_task"
|
|
12
|
+
|
|
13
|
+
RuboCop::RakeTask.new do |task|
|
|
14
|
+
task.requires << 'rubocop-rspec'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
task default: %i[spec rubocop]
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "httpigeon"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require "irb"
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/httpigeon.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/httpigeon/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "httpigeon"
|
|
7
|
+
spec.version = HTTPigeon::VERSION
|
|
8
|
+
spec.authors = ["2k-joker"]
|
|
9
|
+
spec.email = ["opensource@dailypay.com"]
|
|
10
|
+
spec.licenses = ["MIT"]
|
|
11
|
+
|
|
12
|
+
spec.summary = "Simple, easy way to make and log HTTP requests and responses"
|
|
13
|
+
spec.description = "Client library that simplifies making and logging HTTP requests and responses. This library is built as an abstraction on top of the Faraday ruby client."
|
|
14
|
+
spec.homepage = "https://github.com/dailypay/#{spec.name}"
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new("~> 3.1.0")
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
spec.bindir = "exe"
|
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
29
|
+
|
|
30
|
+
spec.add_dependency "faraday", "~> 2.7.6"
|
|
31
|
+
spec.add_dependency "activesupport", "~> 7.0.4"
|
|
32
|
+
|
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
|
35
|
+
spec.add_development_dependency "simplecov"
|
|
36
|
+
spec.add_development_dependency "rubocop", "~> 1.21"
|
|
37
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.24"
|
|
38
|
+
spec.add_development_dependency "pry", "~> 0.13.1"
|
|
39
|
+
|
|
40
|
+
# For more information and examples about making a new gem, check out our
|
|
41
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
42
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module HTTPigeon
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :default_event_type, :default_filter_keys, :redactor_string, :log_redactor, :event_logger, :notify_all_exceptions, :exception_notifier, :auto_generate_request_id
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@default_event_type = 'http.outbound'
|
|
7
|
+
@default_filter_keys = []
|
|
8
|
+
@redactor_string = '[FILTERED]'
|
|
9
|
+
@log_redactor = nil
|
|
10
|
+
@event_logger = nil
|
|
11
|
+
@auto_generate_request_id = true
|
|
12
|
+
@notify_all_exceptions = false
|
|
13
|
+
@exception_notifier = nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "active_support/core_ext/hash"
|
|
2
|
+
|
|
3
|
+
module HTTPigeon
|
|
4
|
+
class LogRedactor
|
|
5
|
+
class UnsupportedRegexpError < StandardError; end
|
|
6
|
+
|
|
7
|
+
attr_reader :log_filters
|
|
8
|
+
|
|
9
|
+
def initialize(log_filters: nil)
|
|
10
|
+
@log_filters = log_filters.to_a.map(&:to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def redact(data)
|
|
14
|
+
case data
|
|
15
|
+
when Array
|
|
16
|
+
data.map { |datum| redact(datum) }
|
|
17
|
+
when String
|
|
18
|
+
redact_string(data)
|
|
19
|
+
when Hash
|
|
20
|
+
redact_hash(data)
|
|
21
|
+
else
|
|
22
|
+
data
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def redact_value(value)
|
|
29
|
+
length = value.to_s.length
|
|
30
|
+
|
|
31
|
+
return value unless length.positive?
|
|
32
|
+
|
|
33
|
+
case length
|
|
34
|
+
when 1..4
|
|
35
|
+
HTTPigeon.redactor_string
|
|
36
|
+
when 5..16
|
|
37
|
+
"#{value.to_s[0..2]}...#{HTTPigeon.redactor_string}"
|
|
38
|
+
when 17..32
|
|
39
|
+
"#{value.to_s[0..(length / 4)]}...#{HTTPigeon.redactor_string}"
|
|
40
|
+
else
|
|
41
|
+
"#{value.to_s[0..5]}...#{HTTPigeon.redactor_string}...#{value.to_s[-6..]}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def redact_hash(data)
|
|
46
|
+
data.to_h do |k, v|
|
|
47
|
+
filter = hash_filter_for(k)
|
|
48
|
+
|
|
49
|
+
if filter.present?
|
|
50
|
+
replacement = filter.split('::')[1].presence
|
|
51
|
+
v = replacement.present? ? replacement : redact_value(v)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if v.is_a?(Hash)
|
|
55
|
+
[k, redact_hash(v)]
|
|
56
|
+
elsif v.is_a?(Array)
|
|
57
|
+
[k, v.map { |val| redact(val) }]
|
|
58
|
+
else
|
|
59
|
+
[k, v]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def redact_string(data)
|
|
65
|
+
log_filters.each do |filter|
|
|
66
|
+
pattern, replacement = filter.split('::')
|
|
67
|
+
|
|
68
|
+
next unless pattern.match?(%r{^/.*/([guysim]*)$})
|
|
69
|
+
|
|
70
|
+
data = if replacement.present?
|
|
71
|
+
data.gsub(regex_for(pattern), replacement)
|
|
72
|
+
else
|
|
73
|
+
data.gsub(regex_for(pattern)) do |sub|
|
|
74
|
+
captures = sub.match(regex_for(pattern))&.captures
|
|
75
|
+
|
|
76
|
+
captures.present? ? captures[0] + redact_value(captures[1]) : sub
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
data
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def hash_filter_for(key)
|
|
85
|
+
log_filters.detect { |k| k.split('::').first.downcase == key.to_s.downcase }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def regex_for(pattern)
|
|
89
|
+
regexp_literal = (pattern.match %r{^(/)(.*)(/(i?))$}).to_a[2].to_s
|
|
90
|
+
|
|
91
|
+
raise UnsupportedRegexpError, "The specified regexp is invalid: #{pattern}. NOTE: Only ignore case (/i) is currently supported." if regexp_literal.blank?
|
|
92
|
+
|
|
93
|
+
Regexp.new(regexp_literal, Regexp::IGNORECASE)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "active_support/core_ext/hash"
|
|
2
|
+
require "active_support/core_ext/object/deep_dup"
|
|
3
|
+
|
|
4
|
+
module HTTPigeon
|
|
5
|
+
class Logger
|
|
6
|
+
attr_reader :event_type, :log_redactor, :start_time, :end_time
|
|
7
|
+
|
|
8
|
+
def initialize(event_type: nil, log_filters: nil)
|
|
9
|
+
@event_type = event_type || HTTPigeon.default_event_type
|
|
10
|
+
@log_redactor = HTTPigeon.log_redactor || HTTPigeon::LogRedactor.new(log_filters: HTTPigeon.default_filter_keys | log_filters.to_a)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def log(faraday_env, data = {})
|
|
14
|
+
base_log_data = { event_type: event_type }
|
|
15
|
+
log_data = build_log_data(faraday_env, data).merge(base_log_data)
|
|
16
|
+
|
|
17
|
+
HTTPigeon.event_logger.nil? ? log_to_stdout(log_data) : HTTPigeon.event_logger.log(log_data)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
HTTPigeon.exception_notifier.notify_exception(e) if HTTPigeon.notify_all_exceptions
|
|
20
|
+
raise e if ['development', 'test'].include?(ENV['RAILS_ENV'].to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_request_start
|
|
24
|
+
@start_time = Time.current
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_request_finish
|
|
28
|
+
@end_time = Time.current
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def build_log_data(env, data)
|
|
34
|
+
log_data = data.deep_dup
|
|
35
|
+
request_id = env.request_headers.transform_keys(&:downcase)['x-request-id']
|
|
36
|
+
request_latency = end_time - start_time if end_time.present? && start_time.present?
|
|
37
|
+
|
|
38
|
+
log_data[:request] = {
|
|
39
|
+
method: env.method,
|
|
40
|
+
url: redact(env.url.to_s),
|
|
41
|
+
headers: redact(env.request_headers),
|
|
42
|
+
body: redact(env.request_body),
|
|
43
|
+
host: env.url.host,
|
|
44
|
+
path: env.url.path
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log_data[:response] = {
|
|
48
|
+
headers: redact(env.response_headers),
|
|
49
|
+
body: redact(env.response_body),
|
|
50
|
+
status: env.status
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log_data[:metadata] = {
|
|
54
|
+
latency: request_latency,
|
|
55
|
+
identifier: request_id,
|
|
56
|
+
protocol: env.url.scheme
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if log_data[:error].present?
|
|
60
|
+
error = log_data.delete(:error)
|
|
61
|
+
log_data[:error] = {
|
|
62
|
+
type: error.class.name,
|
|
63
|
+
message: error.message,
|
|
64
|
+
backtrace: error.backtrace.last(10)
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
log_data
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def redact(data)
|
|
72
|
+
return {} if data.blank?
|
|
73
|
+
|
|
74
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
75
|
+
log_redactor.redact(data)
|
|
76
|
+
rescue JSON::ParserError
|
|
77
|
+
log_redactor.redact(data)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def log_to_stdout(log_data)
|
|
81
|
+
::Logger.new($stdout).log(1, log_data.to_json)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "faraday/middleware_registry"
|
|
2
|
+
require "faraday/middleware"
|
|
3
|
+
require "faraday/response"
|
|
4
|
+
|
|
5
|
+
module HTTPigeon
|
|
6
|
+
module Middleware
|
|
7
|
+
class HTTPigeonLogger < Faraday::Middleware
|
|
8
|
+
def initialize(app, logger)
|
|
9
|
+
super(app)
|
|
10
|
+
|
|
11
|
+
@logger = logger
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
logger.on_request_start if logger.respond_to?(:on_request_start)
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
logger.on_request_finish if logger.respond_to?(:on_request_finish)
|
|
20
|
+
logger.log(env, { error: e })
|
|
21
|
+
|
|
22
|
+
raise e
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_complete(env)
|
|
26
|
+
logger.on_request_finish if logger.respond_to?(:on_request_finish)
|
|
27
|
+
logger.log(env)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :logger
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Faraday::Response.register_middleware(httpigeon_logger: HTTPigeon::Middleware::HTTPigeonLogger)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "active_support/core_ext/hash"
|
|
3
|
+
require "active_support/isolated_execution_state"
|
|
4
|
+
require "active_support/core_ext/time"
|
|
5
|
+
require_relative "middleware/httpigeon_logger"
|
|
6
|
+
|
|
7
|
+
module HTTPigeon
|
|
8
|
+
class Request
|
|
9
|
+
class << self
|
|
10
|
+
def get(endpoint, query = {}, headers = {}, event_type = nil, log_filters = [])
|
|
11
|
+
request = new(base_url: endpoint, headers: headers, event_type: event_type, log_filters: log_filters)
|
|
12
|
+
parsed_response = request.run(method: :get, path: '', payload: query) do |req|
|
|
13
|
+
yield(req) if block_given?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
HTTPigeon::Response.new(request, parsed_response, request.response)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def post(endpoint, payload, headers = {}, event_type = nil, log_filters = [])
|
|
20
|
+
request = new(base_url: endpoint, headers: headers, event_type: event_type, log_filters: log_filters)
|
|
21
|
+
parsed_response = request.run(method: :post, path: '', payload: payload) do |req|
|
|
22
|
+
yield(req) if block_given?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
HTTPigeon::Response.new(request, parsed_response, request.response)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :connection, :response, :parsed_response
|
|
30
|
+
|
|
31
|
+
delegate :status, :body, to: :response, prefix: true
|
|
32
|
+
|
|
33
|
+
def initialize(base_url:, options: nil, headers: nil, adapter: nil, logger: nil, event_type: nil, log_filters: nil)
|
|
34
|
+
@base_url = base_url
|
|
35
|
+
@event_type = event_type
|
|
36
|
+
@log_filters = log_filters || []
|
|
37
|
+
@logger = logger || default_logger
|
|
38
|
+
|
|
39
|
+
request_headers = default_headers.merge(headers.to_h)
|
|
40
|
+
base_connection = Faraday.new(url: base_url)
|
|
41
|
+
|
|
42
|
+
@connection = if block_given?
|
|
43
|
+
yield(base_connection) && base_connection
|
|
44
|
+
else
|
|
45
|
+
base_connection.tap do |faraday|
|
|
46
|
+
faraday.headers.deep_merge!(request_headers)
|
|
47
|
+
faraday.options.merge!(options.to_h)
|
|
48
|
+
faraday.request :url_encoded
|
|
49
|
+
faraday.adapter adapter || Faraday.default_adapter
|
|
50
|
+
faraday.response :httpigeon_logger, @logger
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run(method: :get, path: '/', payload: {})
|
|
56
|
+
unless method.to_sym == :get
|
|
57
|
+
payload = payload.presence&.to_json
|
|
58
|
+
connection.headers['Content-Type'] = 'application/json'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@response = connection.send(method, path, payload) do |request|
|
|
62
|
+
yield(request) if block_given?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@parsed_response = parse_response || {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
attr_reader :path, :logger, :event_type, :log_filters
|
|
71
|
+
|
|
72
|
+
def parse_response
|
|
73
|
+
JSON.parse(response_body).with_indifferent_access
|
|
74
|
+
rescue JSON::ParserError
|
|
75
|
+
response_body.presence
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def default_logger
|
|
79
|
+
HTTPigeon::Logger.new(event_type: event_type, log_filters: log_filters)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def default_headers
|
|
83
|
+
HTTPigeon.auto_generate_request_id ? { 'Accept' => 'application/json', 'X-Request-Id' => SecureRandom.uuid } : { 'Accept' => 'application/json' }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class Response
|
|
88
|
+
attr_reader :request, :parsed_response, :raw_response
|
|
89
|
+
|
|
90
|
+
def initialize(request, parsed_response, raw_response)
|
|
91
|
+
@request = request
|
|
92
|
+
@parsed_response = parsed_response
|
|
93
|
+
@raw_response = raw_response
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/httpigeon.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require "active_support/core_ext/module/delegation"
|
|
2
|
+
|
|
3
|
+
require "httpigeon/configuration"
|
|
4
|
+
require "httpigeon/version"
|
|
5
|
+
require "httpigeon/log_redactor"
|
|
6
|
+
require "httpigeon/logger"
|
|
7
|
+
require "httpigeon/request"
|
|
8
|
+
|
|
9
|
+
module HTTPigeon
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
module FilterPatterns
|
|
13
|
+
EMAIL = "/(?'key'(email_?(address|Address)?=))(?'value'(.*\\.[a-z]+))(&|$)/".freeze
|
|
14
|
+
PASSWORD = "/(?'key'(pass_?(w|W)?ord=))(?'value'([^&$])*)/".freeze
|
|
15
|
+
USERNAME = "/(?'key'(user_?(n|N)?ame=))(?'value'([^&$])*)/".freeze
|
|
16
|
+
CLIENT_ID = "/(?'key'(client_?(id|Id)?=))(?'value'([^&$])*)/".freeze
|
|
17
|
+
CLIENT_SECRET = "/(?'key'(client_?(s|S)?ecret=))(?'value'([^&$])*)/".freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
delegate :default_event_type,
|
|
21
|
+
:default_filter_keys,
|
|
22
|
+
:redactor_string,
|
|
23
|
+
:log_redactor,
|
|
24
|
+
:event_logger,
|
|
25
|
+
:auto_generate_request_id,
|
|
26
|
+
:notify_all_exceptions,
|
|
27
|
+
:exception_notifier,
|
|
28
|
+
to: :configuration
|
|
29
|
+
|
|
30
|
+
def configure
|
|
31
|
+
@config = HTTPigeon::Configuration.new
|
|
32
|
+
|
|
33
|
+
yield(@config)
|
|
34
|
+
|
|
35
|
+
@config.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def configuration
|
|
41
|
+
@configuration ||= @config || HTTPigeon::Configuration.new
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: httpigeon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- 2k-joker
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2023-10-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 2.7.6
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 2.7.6
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 7.0.4
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 7.0.4
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.4'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.4'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: simplecov
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.21'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.21'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop-rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '2.24'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '2.24'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: pry
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 0.13.1
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 0.13.1
|
|
125
|
+
description: Client library that simplifies making and logging HTTP requests and responses.
|
|
126
|
+
This library is built as an abstraction on top of the Faraday ruby client.
|
|
127
|
+
email:
|
|
128
|
+
- opensource@dailypay.com
|
|
129
|
+
executables: []
|
|
130
|
+
extensions: []
|
|
131
|
+
extra_rdoc_files: []
|
|
132
|
+
files:
|
|
133
|
+
- ".github/PULL_REQUEST_TEMPLATE/new_feature_template.md"
|
|
134
|
+
- ".github/workflows/ci.yaml"
|
|
135
|
+
- ".github/workflows/publish.yaml"
|
|
136
|
+
- ".github/workflows/release-please.yaml"
|
|
137
|
+
- ".github/workflows/reviewdog.yaml"
|
|
138
|
+
- ".gitignore"
|
|
139
|
+
- ".rubocop.yml"
|
|
140
|
+
- CHANGELOG.md
|
|
141
|
+
- Gemfile
|
|
142
|
+
- LICENSE
|
|
143
|
+
- README.md
|
|
144
|
+
- Rakefile
|
|
145
|
+
- bin/console
|
|
146
|
+
- bin/setup
|
|
147
|
+
- httpigeon.gemspec
|
|
148
|
+
- lib/httpigeon.rb
|
|
149
|
+
- lib/httpigeon/configuration.rb
|
|
150
|
+
- lib/httpigeon/log_redactor.rb
|
|
151
|
+
- lib/httpigeon/logger.rb
|
|
152
|
+
- lib/httpigeon/middleware/httpigeon_logger.rb
|
|
153
|
+
- lib/httpigeon/request.rb
|
|
154
|
+
- lib/httpigeon/version.rb
|
|
155
|
+
homepage: https://github.com/dailypay/httpigeon
|
|
156
|
+
licenses:
|
|
157
|
+
- MIT
|
|
158
|
+
metadata:
|
|
159
|
+
homepage_uri: https://github.com/dailypay/httpigeon
|
|
160
|
+
source_code_uri: https://github.com/dailypay/httpigeon
|
|
161
|
+
post_install_message:
|
|
162
|
+
rdoc_options: []
|
|
163
|
+
require_paths:
|
|
164
|
+
- lib
|
|
165
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - "~>"
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: 3.1.0
|
|
170
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
|
+
requirements:
|
|
172
|
+
- - ">="
|
|
173
|
+
- !ruby/object:Gem::Version
|
|
174
|
+
version: '0'
|
|
175
|
+
requirements: []
|
|
176
|
+
rubygems_version: 3.3.26
|
|
177
|
+
signing_key:
|
|
178
|
+
specification_version: 4
|
|
179
|
+
summary: Simple, easy way to make and log HTTP requests and responses
|
|
180
|
+
test_files: []
|