umbrellio-utils 1.14.2 → 1.15.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 +4 -4
- data/.github/workflows/release.yml +33 -21
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +26 -0
- data/lib/umbrellio_utils/large_object.rb +1 -3
- data/lib/umbrellio_utils/patches/active_support_event.rb +64 -0
- data/lib/umbrellio_utils/patches/rails_semantic_logger.rb +62 -0
- data/lib/umbrellio_utils/semantic_logger/sidekiq_job_metrics.rb +44 -0
- data/lib/umbrellio_utils/semantic_logger/tiny_json_formatter.rb +21 -1
- data/lib/umbrellio_utils/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c27f256cd436b6d013e2e51538fd44b9fd8004a094f10c24d5bb169d932ddfbb
|
|
4
|
+
data.tar.gz: 68866be3591d51653d2099439bc9c1d75e627745ee75f2e7971313298c8b8a36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6043a41a4db19e94a620be02aeb9481f44eaab456b944f840fa185f467a6b74abdd58ed809c5505dd736c7d77caca990ea8e26f251552dd3227cbf448fd6612d
|
|
7
|
+
data.tar.gz: 7fc84ce2916828b0d67254ac6dcc25e92a9d9b05c6e865f27ef969a2bb7a6f7543056e48b726df522d7aa31a210a8c27f39198dfdb1bfa0b4531c07e81824a6a
|
|
@@ -2,41 +2,53 @@ name: Release
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
6
|
-
-
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
|
-
|
|
10
|
-
runs-on: ubuntu-
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
11
13
|
steps:
|
|
12
14
|
- uses: actions/checkout@v4
|
|
13
15
|
|
|
16
|
+
- name: Extract version
|
|
17
|
+
id: version
|
|
18
|
+
run: |
|
|
19
|
+
version=$(grep -oP '(?<=VERSION = ")[^"]+' lib/umbrellio_utils/version.rb)
|
|
20
|
+
echo "tag=v$version" >> "$GITHUB_OUTPUT"
|
|
21
|
+
|
|
22
|
+
- name: Check if release already exists
|
|
23
|
+
id: check
|
|
24
|
+
run: |
|
|
25
|
+
if gh release view "${{ steps.version.outputs.tag }}" &>/dev/null; then
|
|
26
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
27
|
+
else
|
|
28
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
29
|
+
fi
|
|
30
|
+
env:
|
|
31
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
|
|
14
33
|
- uses: ruby/setup-ruby@v1
|
|
34
|
+
if: steps.check.outputs.exists == 'false'
|
|
15
35
|
with:
|
|
16
36
|
ruby-version: "3.4"
|
|
17
37
|
bundler-cache: true
|
|
18
38
|
|
|
19
39
|
- name: Build gem
|
|
20
|
-
|
|
40
|
+
if: steps.check.outputs.exists == 'false'
|
|
41
|
+
run: gem build umbrellio_utils.gemspec
|
|
21
42
|
|
|
22
|
-
- name: Push to RubyGems
|
|
23
|
-
|
|
43
|
+
- name: Push gem to RubyGems
|
|
44
|
+
if: steps.check.outputs.exists == 'false'
|
|
45
|
+
run: gem push *.gem
|
|
24
46
|
env:
|
|
25
47
|
GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }}
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
runs-on: ubuntu-24.04
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
steps:
|
|
33
|
-
- uses: actions/checkout@v4
|
|
34
|
-
|
|
35
|
-
- name: Create GitHub release
|
|
36
|
-
uses: actions/create-release@v1
|
|
49
|
+
- uses: softprops/action-gh-release@v3
|
|
50
|
+
if: steps.check.outputs.exists == 'false'
|
|
37
51
|
with:
|
|
38
|
-
tag_name: ${{
|
|
39
|
-
|
|
52
|
+
tag_name: ${{ steps.version.outputs.tag }}
|
|
53
|
+
files: "*.gem"
|
|
40
54
|
generate_release_notes: true
|
|
41
|
-
env:
|
|
42
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -11,7 +11,7 @@ GIT
|
|
|
11
11
|
PATH
|
|
12
12
|
remote: .
|
|
13
13
|
specs:
|
|
14
|
-
umbrellio-utils (1.
|
|
14
|
+
umbrellio-utils (1.15.0)
|
|
15
15
|
memery (~> 1)
|
|
16
16
|
|
|
17
17
|
GEM
|
|
@@ -139,6 +139,7 @@ GEM
|
|
|
139
139
|
net-http (~> 0.5)
|
|
140
140
|
globalid (1.3.0)
|
|
141
141
|
activesupport (>= 6.1)
|
|
142
|
+
gvltools (0.5.0)
|
|
142
143
|
http (6.0.3)
|
|
143
144
|
http-cookie (~> 1.0)
|
|
144
145
|
llhttp (~> 0.6.1)
|
|
@@ -415,6 +416,7 @@ DEPENDENCIES
|
|
|
415
416
|
click_house!
|
|
416
417
|
clickhouse-native
|
|
417
418
|
csv
|
|
419
|
+
gvltools
|
|
418
420
|
http
|
|
419
421
|
net-pop
|
|
420
422
|
nokogiri
|
data/README.md
CHANGED
|
@@ -115,6 +115,32 @@ end
|
|
|
115
115
|
Utils::Constants.useful_method #=> "Just string"
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
### Instrumentation
|
|
119
|
+
|
|
120
|
+
The gem ships a set of opt-in files for collecting GVL and allocation stats
|
|
121
|
+
(they are not loaded by default and require the `gvltools` gem in your app):
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# Extends ActiveSupport::Notifications::Event with #gvl_time and #malloc_increase_bytes
|
|
125
|
+
require "umbrellio_utils/patches/active_support_event"
|
|
126
|
+
|
|
127
|
+
# Enriches the "Completed" action controller log entry (rails_semantic_logger)
|
|
128
|
+
# with GC, GVL and allocation stats
|
|
129
|
+
require "umbrellio_utils/patches/rails_semantic_logger"
|
|
130
|
+
|
|
131
|
+
# Logs Sidekiq job completion with the same stats. Relies on the
|
|
132
|
+
# "perform.sidekiq_job" notification published by the umbrellio fork of yabeda-sidekiq
|
|
133
|
+
require "umbrellio_utils/semantic_logger/sidekiq_job_metrics"
|
|
134
|
+
UmbrellioUtils::SemanticLogger::SidekiqJobMetrics.subscribe!
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Don't forget to enable the GVL timer as early as possible (e.g. right after
|
|
138
|
+
`Bundler.require` in `config/application.rb`):
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
GVLTools::LocalTimer.enable
|
|
142
|
+
```
|
|
143
|
+
|
|
118
144
|
## Contributing
|
|
119
145
|
|
|
120
146
|
Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/utils.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gvltools"
|
|
4
|
+
require "active_support/notifications"
|
|
5
|
+
|
|
6
|
+
module UmbrellioUtils
|
|
7
|
+
# Namespace for opt-in monkey-patches. None of these files are loaded by default,
|
|
8
|
+
# require them explicitly from your application.
|
|
9
|
+
module Patches
|
|
10
|
+
# Extends ActiveSupport::Notifications::Event with GVL wait time and malloc stats.
|
|
11
|
+
# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/notifications/instrumenter.rb # rubocop:disable Layout/LineLength
|
|
12
|
+
#
|
|
13
|
+
# The GVL timer must be enabled in the application as early as possible:
|
|
14
|
+
# GVLTools::LocalTimer.enable
|
|
15
|
+
module ActiveSupportEvent
|
|
16
|
+
STATS_PRECISION = 6
|
|
17
|
+
|
|
18
|
+
def initialize(...)
|
|
19
|
+
super
|
|
20
|
+
@gvl_time_start = 0
|
|
21
|
+
@gvl_time_finish = 0
|
|
22
|
+
@malloc_increase_bytes_start = 0
|
|
23
|
+
@malloc_increase_bytes_finish = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start!
|
|
27
|
+
super
|
|
28
|
+
@gvl_time_start = GVLTools::LocalTimer.monotonic_time
|
|
29
|
+
@malloc_increase_bytes_start = GC.stat(:malloc_increase_bytes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def finish!
|
|
33
|
+
super
|
|
34
|
+
@gvl_time_finish = GVLTools::LocalTimer.monotonic_time
|
|
35
|
+
@malloc_increase_bytes_finish = GC.stat(:malloc_increase_bytes)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Time the thread spent waiting for the GVL, in milliseconds
|
|
39
|
+
def gvl_time
|
|
40
|
+
(@gvl_time_finish - @gvl_time_start) / 1_000_000.0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Delta of the process-global malloc counter: can be negative (it resets on GC)
|
|
44
|
+
# and includes sibling threads' allocations under threaded servers. Guard with
|
|
45
|
+
# #positive? where a counter is expected.
|
|
46
|
+
def malloc_increase_bytes
|
|
47
|
+
@malloc_increase_bytes_finish - @malloc_increase_bytes_start
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stats(precision: STATS_PRECISION)
|
|
51
|
+
{
|
|
52
|
+
gc_time: gc_time.round(precision),
|
|
53
|
+
gvl_time: gvl_time.round(precision),
|
|
54
|
+
cpu_time: cpu_time.round(precision),
|
|
55
|
+
idle_time: idle_time.round(precision),
|
|
56
|
+
allocations:,
|
|
57
|
+
malloc_increase_bytes:,
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
ActiveSupport::Notifications::Event.prepend(UmbrellioUtils::Patches::ActiveSupportEvent)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "active_support_event"
|
|
4
|
+
|
|
5
|
+
module UmbrellioUtils
|
|
6
|
+
module Patches
|
|
7
|
+
# Simplifies the "Completed" action controller log entry and enriches it with
|
|
8
|
+
# GC, GVL and allocation stats.
|
|
9
|
+
# https://github.com/reidmorrison/rails_semantic_logger/blob/master/lib/rails_semantic_logger/action_controller/log_subscriber.rb # rubocop:disable Layout/LineLength
|
|
10
|
+
#
|
|
11
|
+
# Require this file after the rails_semantic_logger gem has been loaded
|
|
12
|
+
# (e.g. from an initializer).
|
|
13
|
+
module RailsSemanticLogger
|
|
14
|
+
PRECISION = 6
|
|
15
|
+
PARAMS_SIZE_LIMIT = 10_000
|
|
16
|
+
|
|
17
|
+
def process_action(event)
|
|
18
|
+
::Rails.logger.info do
|
|
19
|
+
payload = event.payload.dup
|
|
20
|
+
payload[:path] = extract_path(payload[:path]) if payload.key?(:path)
|
|
21
|
+
payload[:params] = cap_params(payload[:params]) if payload.key?(:params)
|
|
22
|
+
|
|
23
|
+
payload[:view_time] = payload.delete(:view_runtime).to_f.round(PRECISION)
|
|
24
|
+
payload[:db_time] = payload.delete(:db_runtime).to_f.round(PRECISION)
|
|
25
|
+
payload.merge!(event.stats)
|
|
26
|
+
|
|
27
|
+
# Causes excessive log output with Rails 5 RC1
|
|
28
|
+
payload.delete(:headers)
|
|
29
|
+
# Causes recursion in Rails 6.1.rc1
|
|
30
|
+
payload.delete(:request)
|
|
31
|
+
payload.delete(:response)
|
|
32
|
+
# Keep only the [class, message] pair in :exception (set by ActiveSupport),
|
|
33
|
+
# not the raw exception with its backtrace — consistent with sidekiq_job_metrics.
|
|
34
|
+
payload.delete(:exception_object)
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
message: "Completed ##{payload[:action]}",
|
|
38
|
+
duration: event.duration,
|
|
39
|
+
payload:,
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def cap_params(params)
|
|
47
|
+
serialized = params.to_json
|
|
48
|
+
return params if serialized.bytesize <= PARAMS_SIZE_LIMIT
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
truncated: true,
|
|
52
|
+
bytesize: serialized.bytesize,
|
|
53
|
+
preview: "#{serialized[0, PARAMS_SIZE_LIMIT]}...",
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
RailsSemanticLogger::ActionController::LogSubscriber.prepend(
|
|
61
|
+
UmbrellioUtils::Patches::RailsSemanticLogger,
|
|
62
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../patches/active_support_event"
|
|
4
|
+
|
|
5
|
+
module UmbrellioUtils
|
|
6
|
+
module SemanticLogger
|
|
7
|
+
# Logs Sidekiq job duration, GC, GVL and allocation stats as a dedicated line.
|
|
8
|
+
# Relies on the "perform.sidekiq_job" notification published by the umbrellio
|
|
9
|
+
# fork of yabeda-sidekiq. Call +subscribe!+ from an initializer.
|
|
10
|
+
module SidekiqJobMetrics
|
|
11
|
+
MESSAGE = "Sidekiq job stats"
|
|
12
|
+
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
def subscribe!
|
|
16
|
+
ActiveSupport::Notifications.subscribe("perform.sidekiq_job") do |event|
|
|
17
|
+
log_event(event)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def log_event(event)
|
|
24
|
+
logger = ::SemanticLogger[event.payload[:worker] || "Sidekiq"]
|
|
25
|
+
logger.info(
|
|
26
|
+
message: MESSAGE,
|
|
27
|
+
duration: event.duration,
|
|
28
|
+
payload: metrics_payload(event),
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def metrics_payload(event)
|
|
33
|
+
payload = {
|
|
34
|
+
worker: event.payload[:worker],
|
|
35
|
+
queue: event.payload[:queue],
|
|
36
|
+
**event.stats,
|
|
37
|
+
}
|
|
38
|
+
# [class, message] pair set by ActiveSupport::Notifications when the job raised
|
|
39
|
+
payload[:exception] = event.payload[:exception] if event.payload[:exception]
|
|
40
|
+
payload
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -20,6 +20,10 @@ module UmbrellioUtils
|
|
|
20
20
|
time: :time,
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
|
+
# Fields that are included in the output only when explicitly requested
|
|
24
|
+
# via +custom_names_mapping+ (e.g. <code>{ duration: :duration }</code>).
|
|
25
|
+
OPTIONAL_FIELDS = %i[duration payload].freeze
|
|
26
|
+
|
|
23
27
|
# Returns a new instance of the {UmbrellioUtils::SemanticLogger::TinyJsonFormatter}.
|
|
24
28
|
# @option [Integer] message_size_limit maximum number of characters in a log message
|
|
25
29
|
# @option [Hash] custom_names_mapping mapping from default field names to custom ones.
|
|
@@ -31,6 +35,12 @@ module UmbrellioUtils
|
|
|
31
35
|
# @option custom_names_mapping [Symbol] :tags custom name for the `tags` field.
|
|
32
36
|
# @option custom_names_mapping [Symbol] :named_tags custom name for the `named_tags` field.
|
|
33
37
|
# @option custom_names_mapping [Symbol] :time custom name for the `time` field.
|
|
38
|
+
# @option custom_names_mapping [Symbol] :duration enables logging of the `duration`
|
|
39
|
+
# field under the given name. Unlike the default fields, it is not logged
|
|
40
|
+
# unless explicitly listed in the mapping.
|
|
41
|
+
# @option custom_names_mapping [Symbol] :payload enables logging of the `payload`
|
|
42
|
+
# field under the given name. Unlike the default fields, it is not logged
|
|
43
|
+
# unless explicitly listed in the mapping.
|
|
34
44
|
# @example Use custom name for the `message` and `time` fields
|
|
35
45
|
# UmbrellioUtils::SemanticLogger::TinyJsonFormatter.new(
|
|
36
46
|
# time: :timestamp, message: :note,
|
|
@@ -60,7 +70,17 @@ module UmbrellioUtils
|
|
|
60
70
|
# Builds hash with data from log.
|
|
61
71
|
# @return [Hash] the hash, which will be converted to the JSON later.
|
|
62
72
|
def build_data_for(log)
|
|
63
|
-
field_names.values_at(*DEFAULT_NAMES_MAPPING.keys).zip(pack_data(log)).to_h
|
|
73
|
+
data = field_names.values_at(*DEFAULT_NAMES_MAPPING.keys).zip(pack_data(log)).to_h
|
|
74
|
+
|
|
75
|
+
OPTIONAL_FIELDS.each do |field|
|
|
76
|
+
field_name = field_names[field]
|
|
77
|
+
next if field_name.nil?
|
|
78
|
+
|
|
79
|
+
value = log.public_send(field)
|
|
80
|
+
data[field_name] = value unless value.nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
data
|
|
64
84
|
end
|
|
65
85
|
|
|
66
86
|
# Builds an [Array] with all the required fields, which are arranged
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: umbrellio-utils
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Team Umbrellio
|
|
@@ -68,11 +68,14 @@ files:
|
|
|
68
68
|
- lib/umbrellio_utils/misc.rb
|
|
69
69
|
- lib/umbrellio_utils/parsing.rb
|
|
70
70
|
- lib/umbrellio_utils/passwords.rb
|
|
71
|
+
- lib/umbrellio_utils/patches/active_support_event.rb
|
|
72
|
+
- lib/umbrellio_utils/patches/rails_semantic_logger.rb
|
|
71
73
|
- lib/umbrellio_utils/random.rb
|
|
72
74
|
- lib/umbrellio_utils/request_wrapper.rb
|
|
73
75
|
- lib/umbrellio_utils/rounding.rb
|
|
74
76
|
- lib/umbrellio_utils/rubocop.rb
|
|
75
77
|
- lib/umbrellio_utils/rubocop/cop/custom/regex_rules.rb
|
|
78
|
+
- lib/umbrellio_utils/semantic_logger/sidekiq_job_metrics.rb
|
|
76
79
|
- lib/umbrellio_utils/semantic_logger/tiny_json_formatter.rb
|
|
77
80
|
- lib/umbrellio_utils/sql.rb
|
|
78
81
|
- lib/umbrellio_utils/store.rb
|