logtail 0.1.18 → 0.1.20
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 +4 -4
- data/.github/workflows/release.yml +37 -33
- data/lib/logtail/config.rb +4 -0
- data/lib/logtail/log_devices/http.rb +18 -4
- data/lib/logtail/version.rb +1 -1
- data/spec/logtail/config_spec.rb +16 -0
- data/spec/logtail/log_devices/http_spec.rb +81 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d8397873f23b58750258e46a3162866eaa3ab6ffaf551078d28b25b44b5ad69
|
|
4
|
+
data.tar.gz: 83a5def59147d9b94a890db6e7f07dd2107bdfa68ba7eb929ca47fa5a44a212b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d710225c254e58dbdfa66e6e87515167fcc651ce4cd14d7ddeb6cb682d9b547f8eba13c9729a939249ca565229fcd68e4f9867db17900e798051abe5b0699473
|
|
7
|
+
data.tar.gz: 5d785a4b048a73403e2d389780d5e83702b0ada279fb2dd727e2b192176f15133620668e6f2a6eccdb5b142a29d9283fd03a3058f2d3f31aace47f42794ca820
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
# Publishes the gem to RubyGems.org from GitHub Actions, the way
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# Publishes the gem to RubyGems.org from GitHub Actions with trusted publishing (OIDC), the way
|
|
2
|
+
# logtail-js releases the @logtail/* packages to npm. No RubyGems.org API key exists anywhere:
|
|
3
|
+
# the gem on rubygems.org trusts exactly this workflow file, run from this repository in the
|
|
4
|
+
# "rubygems" environment, and the shared account's multi-factor authentication stays as it is.
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# One-time setup on rubygems.org, signed in as the gem's owner: the gem's page → Trusted publishers
|
|
7
|
+
# → GitHub Actions, with the repository owner "logtail", this repository's name, the workflow
|
|
8
|
+
# "release.yml" and the environment "rubygems".
|
|
8
9
|
#
|
|
9
10
|
# Repository settings this relies on: the "rubygems" environment with deployment branches limited
|
|
10
11
|
# to main and no required reviewers (add reviewers there if releases should need an approval; a
|
|
@@ -17,15 +18,17 @@
|
|
|
17
18
|
# both, pushes the gem to RubyGems.org and creates a GitHub release with auto-generated notes for
|
|
18
19
|
# the tag; edit the notes afterwards if needed.
|
|
19
20
|
#
|
|
20
|
-
# Retry: if a release failed after the version commit was pushed, run "retry"
|
|
21
|
-
#
|
|
22
|
-
# missing it and creates the GitHub release if it is still
|
|
23
|
-
# would release the next version instead.
|
|
21
|
+
# Retry: if a release failed after the version commit was pushed, run "retry" from main. It bumps
|
|
22
|
+
# nothing, checks out the tag of the version in lib/*/version.rb and rebuilds that commit, pushes
|
|
23
|
+
# the gem if RubyGems.org is still missing it and creates the GitHub release if it is still
|
|
24
|
+
# missing. Running patch or minor again would release the next version instead.
|
|
24
25
|
#
|
|
25
|
-
# Dry run: bumps in place without committing, builds the gem and
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
26
|
+
# Dry run: bumps in place without committing, builds the gem and, on main, exchanges the
|
|
27
|
+
# workflow's OIDC token with RubyGems.org, which proves that the trusted publisher matches this
|
|
28
|
+
# workflow. Nothing is pushed. Every push that touches this file is a dry run, so a change to the
|
|
29
|
+
# workflow proves itself on its pull request before it reaches main, and the push that merges it
|
|
30
|
+
# performs the token exchange as well, because the "rubygems" environment only admits main. A dry
|
|
31
|
+
# run can also be dispatched from any branch.
|
|
29
32
|
name: Release
|
|
30
33
|
|
|
31
34
|
on:
|
|
@@ -37,7 +40,7 @@ on:
|
|
|
37
40
|
options: [patch, minor, retry]
|
|
38
41
|
required: true
|
|
39
42
|
dry_run:
|
|
40
|
-
description: "Dry run: bump and build,
|
|
43
|
+
description: "Dry run: bump and build, verify RubyGems.org accepts this workflow, publish nothing"
|
|
41
44
|
type: boolean
|
|
42
45
|
default: false
|
|
43
46
|
push:
|
|
@@ -108,7 +111,9 @@ jobs:
|
|
|
108
111
|
tagged = `git tag --points-at HEAD`.split.include?("v#{current}")
|
|
109
112
|
|
|
110
113
|
if release == "retry"
|
|
111
|
-
|
|
114
|
+
# main may have moved on since the version commit, so build what the tag points at
|
|
115
|
+
abort("retry only finishes a release whose version v#{current} is tagged") unless system("git", "rev-parse", "-q", "--verify", "refs/tags/v#{current}", out: File::NULL)
|
|
116
|
+
system("git", "checkout", "-q", "v#{current}") || abort("could not check out v#{current}")
|
|
112
117
|
version = current
|
|
113
118
|
else
|
|
114
119
|
abort("HEAD is already released as v#{current}, there is nothing new to release") if tagged && !dry_run
|
|
@@ -126,6 +131,7 @@ jobs:
|
|
|
126
131
|
env:
|
|
127
132
|
VERSION: ${{ steps.version.outputs.version }}
|
|
128
133
|
run: |
|
|
134
|
+
git diff --stat
|
|
129
135
|
gem build *.gemspec
|
|
130
136
|
gem=$(ls *.gem)
|
|
131
137
|
case "$gem" in
|
|
@@ -134,18 +140,6 @@ jobs:
|
|
|
134
140
|
esac
|
|
135
141
|
echo "gem=$gem" >> "$GITHUB_OUTPUT"
|
|
136
142
|
|
|
137
|
-
- name: Dry run
|
|
138
|
-
if: ${{ env.DRY_RUN == 'true' }}
|
|
139
|
-
env:
|
|
140
|
-
RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }}
|
|
141
|
-
run: |
|
|
142
|
-
git diff --stat
|
|
143
|
-
if ! grep -q '^:rubygems_api_key: [^ ]' <<< "$RUBYGEMS_CREDENTIALS"; then
|
|
144
|
-
echo "::error::The RUBYGEMS_CREDENTIALS secret must hold the contents of a gem credentials file: ':rubygems_api_key: rubygems_…'"
|
|
145
|
-
exit 1
|
|
146
|
-
fi
|
|
147
|
-
echo "RUBYGEMS_CREDENTIALS looks like a gem credentials file. A release pushes with it, nothing more to verify without pushing."
|
|
148
|
-
|
|
149
143
|
- name: Commit and tag
|
|
150
144
|
if: ${{ env.DRY_RUN != 'true' && env.RELEASE != 'retry' }}
|
|
151
145
|
env:
|
|
@@ -166,11 +160,13 @@ jobs:
|
|
|
166
160
|
release:
|
|
167
161
|
name: Publish
|
|
168
162
|
needs: build
|
|
169
|
-
|
|
163
|
+
# The rubygems environment only admits main, so dry runs from other branches stop after the build
|
|
164
|
+
if: ${{ github.ref == 'refs/heads/main' }}
|
|
170
165
|
runs-on: ubuntu-24.04
|
|
171
166
|
environment: rubygems
|
|
172
167
|
permissions:
|
|
173
168
|
contents: write # creates the GitHub release
|
|
169
|
+
id-token: write # OIDC token exchange with RubyGems.org
|
|
174
170
|
env:
|
|
175
171
|
VERSION: ${{ needs.build.outputs.version }}
|
|
176
172
|
GEM: ${{ needs.build.outputs.gem }}
|
|
@@ -185,20 +181,28 @@ jobs:
|
|
|
185
181
|
with:
|
|
186
182
|
ruby-version: "3"
|
|
187
183
|
|
|
184
|
+
# Exchanges the job's OIDC token for a short-lived RubyGems.org API key and hands it to
|
|
185
|
+
# `gem push`. Fails when no trusted publisher on rubygems.org matches this workflow.
|
|
186
|
+
- uses: rubygems/configure-rubygems-credentials@v2.1.0
|
|
187
|
+
|
|
188
|
+
- name: Dry run
|
|
189
|
+
if: ${{ env.DRY_RUN == 'true' }}
|
|
190
|
+
run: |
|
|
191
|
+
ls -l "$GEM"
|
|
192
|
+
echo "RubyGems.org accepted the OIDC token: the trusted publisher matches this workflow. Nothing is pushed."
|
|
193
|
+
|
|
188
194
|
- name: Push to RubyGems.org
|
|
189
|
-
env
|
|
190
|
-
RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }}
|
|
195
|
+
if: ${{ env.DRY_RUN != 'true' }}
|
|
191
196
|
run: |
|
|
192
197
|
name="${GEM%-$VERSION.gem}"
|
|
193
198
|
if gem specification --remote "$name" --version "$VERSION" > /dev/null 2>&1; then
|
|
194
199
|
echo "$name $VERSION is on rubygems.org already, finishing the release."
|
|
195
200
|
else
|
|
196
|
-
mkdir -p ~/.gem
|
|
197
|
-
(umask 077 && printf '%s\n' "$RUBYGEMS_CREDENTIALS" > ~/.gem/credentials)
|
|
198
201
|
gem push "$GEM"
|
|
199
202
|
fi
|
|
200
203
|
|
|
201
204
|
- name: Create GitHub release
|
|
205
|
+
if: ${{ env.DRY_RUN != 'true' }}
|
|
202
206
|
env:
|
|
203
207
|
GH_TOKEN: ${{ github.token }}
|
|
204
208
|
GH_REPO: ${{ github.repository }}
|
data/lib/logtail/config.rb
CHANGED
|
@@ -32,6 +32,10 @@ module Logtail
|
|
|
32
32
|
|
|
33
33
|
attr_writer :http_body_limit
|
|
34
34
|
|
|
35
|
+
def initialize
|
|
36
|
+
@debug_logger = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
# Whether a particular {Logtail::LogEntry} should be sent to Better Stack
|
|
36
40
|
def send_to_better_stack?(log_entry)
|
|
37
41
|
!@better_stack_filters&.any? { |blocker| blocker.call(log_entry) }
|
|
@@ -22,6 +22,8 @@ module Logtail
|
|
|
22
22
|
DEFAULT_INGESTING_SCHEME = "https".freeze
|
|
23
23
|
CONTENT_TYPE = "application/msgpack".freeze
|
|
24
24
|
USER_AGENT = "Logtail Ruby/#{Logtail::VERSION} (HTTP)".freeze
|
|
25
|
+
INITIAL_RECONNECT_WAIT = 1 # second
|
|
26
|
+
MAX_RECONNECT_WAIT = 30 # seconds
|
|
25
27
|
|
|
26
28
|
# Instantiates a new HTTP log device that can be passed to {Logtail::Logger#initialize}.
|
|
27
29
|
#
|
|
@@ -83,6 +85,7 @@ module Logtail
|
|
|
83
85
|
@request_queue = options[:request_queue] || FlushableDroppingSizedQueue.new(25)
|
|
84
86
|
@successive_error_count = 0
|
|
85
87
|
@requests_in_flight = 0
|
|
88
|
+
@reconnect_wait = INITIAL_RECONNECT_WAIT
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
# Write a new log line message to the buffer, and flush asynchronously if the
|
|
@@ -119,13 +122,13 @@ module Logtail
|
|
|
119
122
|
# Closes the log device, cleans up, and attempts one last delivery.
|
|
120
123
|
def close
|
|
121
124
|
# Kill the flush thread immediately since we are about to flush again.
|
|
122
|
-
@flush_thread.kill if @flush_thread
|
|
125
|
+
@flush_thread.kill.join if @flush_thread
|
|
123
126
|
|
|
124
127
|
# Flush all remaining messages
|
|
125
128
|
flush
|
|
126
129
|
|
|
127
130
|
# Kill the request queue thread. Flushing ensures that no requests are pending.
|
|
128
|
-
@request_outlet_thread.kill if @request_outlet_thread
|
|
131
|
+
@request_outlet_thread.kill.join if @request_outlet_thread
|
|
129
132
|
end
|
|
130
133
|
|
|
131
134
|
def deliver_one(msg)
|
|
@@ -298,15 +301,19 @@ Logtail::Config.instance.debug_logger = ::Logger.new(STDOUT)
|
|
|
298
301
|
http
|
|
299
302
|
end
|
|
300
303
|
|
|
301
|
-
# Creates a loop that processes the `@request_queue` on an interval.
|
|
304
|
+
# Creates a loop that processes the `@request_queue` on an interval. After a failed
|
|
305
|
+
# connection it waits before reconnecting, twice as long after every consecutive
|
|
306
|
+
# failure up to {MAX_RECONNECT_WAIT}, so an unreachable host is not retried in a busy
|
|
307
|
+
# loop. A delivered request starts the wait over (see {#deliver_requests}).
|
|
302
308
|
def request_outlet
|
|
303
309
|
loop do
|
|
304
310
|
http = build_http
|
|
311
|
+
connection_healthy = false
|
|
305
312
|
|
|
306
313
|
begin
|
|
307
314
|
Logtail::Config.instance.debug { "Starting HTTP connection" }
|
|
308
315
|
|
|
309
|
-
http.start do |conn|
|
|
316
|
+
connection_healthy = http.start do |conn|
|
|
310
317
|
deliver_requests(conn)
|
|
311
318
|
end
|
|
312
319
|
rescue => e
|
|
@@ -315,6 +322,12 @@ Logtail::Config.instance.debug_logger = ::Logger.new(STDOUT)
|
|
|
315
322
|
Logtail::Config.instance.debug { "Finishing HTTP connection" }
|
|
316
323
|
http.finish if http.started?
|
|
317
324
|
end
|
|
325
|
+
|
|
326
|
+
next if connection_healthy
|
|
327
|
+
|
|
328
|
+
Logtail::Config.instance.debug { "Reconnecting in #{@reconnect_wait} seconds" }
|
|
329
|
+
sleep(@reconnect_wait)
|
|
330
|
+
@reconnect_wait = [@reconnect_wait * 2, MAX_RECONNECT_WAIT].min
|
|
318
331
|
end
|
|
319
332
|
end
|
|
320
333
|
|
|
@@ -360,6 +373,7 @@ Logtail::Config.instance.debug_logger = ::Logger.new(STDOUT)
|
|
|
360
373
|
num_reqs += 1
|
|
361
374
|
|
|
362
375
|
@last_resp = resp
|
|
376
|
+
@reconnect_wait = INITIAL_RECONNECT_WAIT
|
|
363
377
|
|
|
364
378
|
Logtail::Config.instance.debug do
|
|
365
379
|
if resp.code == "202"
|
data/lib/logtail/version.rb
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Logtail::Config do
|
|
4
|
+
describe "#debug_logger" do
|
|
5
|
+
# Ruby before 3.0 warns under `ruby -w` when an unset instance variable is read, and the
|
|
6
|
+
# HTTP device reads the debug logger on every connection attempt.
|
|
7
|
+
it "can be read before it is set without an uninitialized instance variable warning" do
|
|
8
|
+
config = described_class.send(:new)
|
|
9
|
+
verbose, $VERBOSE = $VERBOSE, true
|
|
10
|
+
|
|
11
|
+
expect { config.debug_logger }.not_to output.to_stderr
|
|
12
|
+
ensure
|
|
13
|
+
$VERBOSE = verbose
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -57,10 +57,8 @@ describe Logtail::LogDevices::HTTP do
|
|
|
57
57
|
http.send(:ensure_flush_threads_are_started)
|
|
58
58
|
http.close
|
|
59
59
|
thread = http.instance_variable_get(:@flush_thread)
|
|
60
|
-
sleep 0.1 # too fast!
|
|
61
60
|
expect(thread).to_not be_alive
|
|
62
61
|
thread = http.instance_variable_get(:@request_outlet_thread)
|
|
63
|
-
sleep 0.1 # too fast!
|
|
64
62
|
expect(thread).to_not be_alive
|
|
65
63
|
end
|
|
66
64
|
|
|
@@ -150,6 +148,87 @@ describe Logtail::LogDevices::HTTP do
|
|
|
150
148
|
|
|
151
149
|
http.close
|
|
152
150
|
end
|
|
151
|
+
|
|
152
|
+
context "when connecting or delivering fails" do
|
|
153
|
+
# Raised from the stubs below to leave the outlet's endless loop. It is not a
|
|
154
|
+
# StandardError, so the outlet's own `rescue => e` lets it through.
|
|
155
|
+
let(:stop_outlet) { Class.new(Exception) }
|
|
156
|
+
let(:http_device) { described_class.new("MYKEY", flush_continuously: false, requests_per_conn: 1) }
|
|
157
|
+
let(:request_queue) { http_device.instance_variable_get(:@request_queue) }
|
|
158
|
+
let(:waits) { [] }
|
|
159
|
+
|
|
160
|
+
before do
|
|
161
|
+
allow(http_device).to receive(:sleep) { |seconds| waits << seconds }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "waits before reconnecting, twice as long after every refused connection, up to 30 seconds" do
|
|
165
|
+
connection_attempts = 0
|
|
166
|
+
allow_any_instance_of(Net::HTTP).to receive(:start) do
|
|
167
|
+
connection_attempts += 1
|
|
168
|
+
raise stop_outlet if connection_attempts > 7
|
|
169
|
+
raise Errno::ECONNREFUSED
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet)
|
|
173
|
+
expect(waits).to eq([1, 2, 4, 8, 16, 30, 30])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "waits the same way when a request fails on an open connection, and still drops it after 3 attempts" do
|
|
177
|
+
request_queue.enq(Logtail::LogDevices::HTTP::RequestAttempt.new(Net::HTTP::Post.new("/")))
|
|
178
|
+
request_attempts = 0
|
|
179
|
+
allow_any_instance_of(Net::HTTP).to receive(:request) do
|
|
180
|
+
request_attempts += 1
|
|
181
|
+
raise Errno::ECONNRESET
|
|
182
|
+
end
|
|
183
|
+
allow(http_device).to receive(:sleep) do |seconds|
|
|
184
|
+
waits << seconds
|
|
185
|
+
raise stop_outlet if waits.size == 3
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet)
|
|
189
|
+
expect(waits).to eq([1, 2, 4])
|
|
190
|
+
expect(request_attempts).to eq(3)
|
|
191
|
+
expect(request_queue.size).to eq(0)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "starts over at 1 second once a request is delivered" do
|
|
195
|
+
request_queue.enq(Logtail::LogDevices::HTTP::RequestAttempt.new(Net::HTTP::Post.new("/")))
|
|
196
|
+
connection = double("connection", request: double("response", code: "202"))
|
|
197
|
+
connections = [:refused, :refused, :delivers, :refused, :refused]
|
|
198
|
+
allow_any_instance_of(Net::HTTP).to receive(:start) do |_http, &block|
|
|
199
|
+
case connections.shift
|
|
200
|
+
when :refused then raise Errno::ECONNREFUSED
|
|
201
|
+
when :delivers then block.call(connection)
|
|
202
|
+
else raise stop_outlet
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet)
|
|
207
|
+
expect(waits).to eq([1, 2, 1, 2])
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "lets close stop the outlet while it waits to reconnect" do
|
|
212
|
+
connection_attempts = 0
|
|
213
|
+
allow_any_instance_of(Net::HTTP).to receive(:start) do
|
|
214
|
+
connection_attempts += 1
|
|
215
|
+
raise Errno::ECONNREFUSED
|
|
216
|
+
end
|
|
217
|
+
http_device = described_class.new("MYKEY")
|
|
218
|
+
http_device.send(:ensure_flush_threads_are_started)
|
|
219
|
+
outlet = http_device.instance_variable_get(:@request_outlet_thread)
|
|
220
|
+
# Up to 5 seconds for the thread's first attempt, which is slow on a cold TruffleRuby.
|
|
221
|
+
500.times do
|
|
222
|
+
break if connection_attempts > 0 && outlet.status == "sleep"
|
|
223
|
+
sleep 0.01
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
closing = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
227
|
+
http_device.close
|
|
228
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - closing).to be < 0.5
|
|
229
|
+
expect(outlet).not_to be_alive
|
|
230
|
+
expect(connection_attempts).to eq(1)
|
|
231
|
+
end
|
|
153
232
|
end
|
|
154
233
|
|
|
155
234
|
describe "#deliver_requests" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logtail
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Better Stack
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- lib/logtail/version.rb
|
|
189
189
|
- logtail.gemspec
|
|
190
190
|
- spec/README.md
|
|
191
|
+
- spec/logtail/config_spec.rb
|
|
191
192
|
- spec/logtail/current_context_spec.rb
|
|
192
193
|
- spec/logtail/events/controller_call_spec.rb
|
|
193
194
|
- spec/logtail/events/error_spec.rb
|