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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2630729cf41d11cf9483cba3097d1c5d949a0042f28a1b32e31e9fa1e227a6ce
4
- data.tar.gz: 446cdaae850b3b8f7907cd02c3ed728df5380faec78e28c41875289a902bd0dd
3
+ metadata.gz: c27f256cd436b6d013e2e51538fd44b9fd8004a094f10c24d5bb169d932ddfbb
4
+ data.tar.gz: 68866be3591d51653d2099439bc9c1d75e627745ee75f2e7971313298c8b8a36
5
5
  SHA512:
6
- metadata.gz: 9febf799edc6661b9f4ebf7187ed014a270371ae6cb0cd859199115038295ffeb65231e07ed26d5a6f17920272011e346075390c3f124a3ec99ccf69bfd81bd2
7
- data.tar.gz: 0062de566624c25277163d639f62e8d1c8080a5ba0a18b3ea1304a296670758a0b92b7d721ab247ae1fcd9c91aef11f760642683ea12c43675bcc1dda3e09336
6
+ metadata.gz: 6043a41a4db19e94a620be02aeb9481f44eaab456b944f840fa185f467a6b74abdd58ed809c5505dd736c7d77caca990ea8e26f251552dd3227cbf448fd6612d
7
+ data.tar.gz: 7fc84ce2916828b0d67254ac6dcc25e92a9d9b05c6e865f27ef969a2bb7a6f7543056e48b726df522d7aa31a210a8c27f39198dfdb1bfa0b4531c07e81824a6a
@@ -2,41 +2,53 @@ name: Release
2
2
 
3
3
  on:
4
4
  push:
5
- tags:
6
- - "v*"
5
+ branches:
6
+ - main
7
7
 
8
8
  jobs:
9
- gem:
10
- runs-on: ubuntu-24.04
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
- run: bundle exec rake build
40
+ if: steps.check.outputs.exists == 'false'
41
+ run: gem build umbrellio_utils.gemspec
21
42
 
22
- - name: Push to RubyGems
23
- run: gem push pkg/*.gem
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
- github-release:
28
- needs: gem
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: ${{ github.ref_name }}
39
- release_name: ${{ github.ref_name }}
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
@@ -11,6 +11,7 @@ gem "ci-helper"
11
11
  gem "click_house", github: "umbrellio/click_house", branch: "master"
12
12
  gem "clickhouse-native"
13
13
  gem "csv"
14
+ gem "gvltools"
14
15
  gem "http"
15
16
  gem "net-pop"
16
17
  gem "nokogiri"
data/Gemfile.lock CHANGED
@@ -11,7 +11,7 @@ GIT
11
11
  PATH
12
12
  remote: .
13
13
  specs:
14
- umbrellio-utils (1.14.2)
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.
@@ -41,9 +41,7 @@ module UmbrellioUtils
41
41
  end
42
42
 
43
43
  def delete!
44
- run(:lo_unlink, oid)
45
- rescue Sequel::DatabaseError
46
- # Ignored
44
+ run(:lo_unlink, oid) if exists?
47
45
  end
48
46
 
49
47
  def exists?
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UmbrellioUtils
4
- VERSION = "1.14.2"
4
+ VERSION = "1.15.0"
5
5
  end
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.14.2
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