nexus_semantic_logger 1.14.1 → 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: 32ad58a45b7f549b78d5a38b15319c90d4e03a254d8c59cd2924bf29f9194501
4
- data.tar.gz: 8999d6c1cef19b0614050b31f193b42e07b10c5d033720890966ce56f137ed5c
3
+ metadata.gz: 0dcbcb0a7a4c7de68e7496b8ffb7f6d5d5e08742a981af559e48162bd149fc40
4
+ data.tar.gz: 4cd8249c19ce91fc5a3f2ef7c1919f91685235cf385291f8e0272ecfaf71234f
5
5
  SHA512:
6
- metadata.gz: dc0dabdf1cdc35d17b22f0f6f166f84802f14d9b16c04b5da4e9ddc9ae42f2c1de9c5eca4f87d62bb6ac30235d03f792e1bed563f18f4f0a04cce230713326a0
7
- data.tar.gz: 3daf0fed3524cf841917e4e43ea6abd9bc4b79ded74fbca066f4cd8b54cecbd13fbf9bf632c55b9942a36e953002a2d6942a2e5feecee3b9c2b1fa9760b1b118
6
+ metadata.gz: 17a71300ae2d6f1bd8c5d0175b167071b818c54ea489d5973a6a2d92cc2fa99dd8f8369015b57520ae8eaf2dfef84f17b30e28c12736637c1620d48ec435d28c
7
+ data.tar.gz: f93a642bc2e30c4913f65cab35cbda108a99d9b15f78fab5f715aa91d53511560d9a2189bd10d02782a90c9b810add8efec1509278bb6108fd4a1ee0b9258416
@@ -0,0 +1,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ rspec:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ gemfile: [rails_7_1, rails_7_2, rails_8_0]
16
+ env:
17
+ BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: "3.2.2"
23
+ - run: bundle install --jobs 4
24
+ - run: bundle exec rspec
25
+
26
+ rubocop:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: "3.2.2"
33
+ - run: bundle install --jobs 4
34
+ - run: bundle exec rubocop
35
+
36
+ release:
37
+ needs: [rspec, rubocop]
38
+ if: startsWith(github.ref, 'refs/tags/')
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: ruby/setup-ruby@v1
43
+ with:
44
+ ruby-version: "3.2.2"
45
+ - run: bundle install --jobs 4
46
+ - name: Build and push gem to RubyGems
47
+ env:
48
+ GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
49
+ run: |
50
+ sed -i "s/0.0.0/${GITHUB_REF_NAME}/g" lib/nexus_semantic_logger/version.rb
51
+ gem build nexus_semantic_logger.gemspec
52
+ gem push nexus_semantic_logger-*.gem
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  .idea/
2
2
  .vscode/
3
3
  results/
4
+ Gemfile.lock
5
+ gemfiles/*.gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
data/Appraisals ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise "rails-7-1" do
4
+ gem "railties", "~> 7.1.0"
5
+ end
6
+
7
+ appraise "rails-7-2" do
8
+ gem "railties", "~> 7.2.0"
9
+ end
10
+
11
+ appraise "rails-8-0" do
12
+ gem "railties", "~> 8.0.0"
13
+ end
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
3
3
 
4
4
  gemspec
5
5
 
6
+ gem 'appraisal'
6
7
  gem 'rubocop'
7
8
  gem 'rubocop-shopify', "~> 1.0.4", require: false
8
9
 
data/README.md CHANGED
@@ -60,6 +60,20 @@ For example, to increment a count:
60
60
  NexusSemanticLogger.metrics.increment('nexus.users.registration.complete')
61
61
  ```
62
62
 
63
+ ### Running shell commands
64
+
65
+ Use `NexusSemanticLogger.shell.capture3` as a drop-in replacement for `Open3.capture3`:
66
+
67
+ ```
68
+ stdout, stderr, status = NexusSemanticLogger.shell.capture3('7z', 'x', archive_path)
69
+ ```
70
+
71
+ Each call is wrapped in a Datadog APM span (`shell.exec`) and emits `nexus.shell.duration_ms`,
72
+ `nexus.shell.peak_rss_mb` (the child's peak RSS, sampled from procfs) and `nexus.shell.executions`
73
+ metrics tagged with `command:<binary>` and `outcome:<success|failure|signaled|exception>`, plus an
74
+ info log line. A child killed by the kernel OOM killer shows up as `outcome:signaled` with
75
+ `termsig: 9` and its last-observed peak RSS.
76
+
63
77
  # Local gem development
64
78
 
65
79
  Steps to run this gem from local sources in one the nexus 'staged build' rails components:
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rubocop"
7
+ gem "rubocop-shopify", "~> 1.0.4", require: false
8
+ gem "rspec"
9
+ gem "railties", "~> 7.1.0"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rubocop"
7
+ gem "rubocop-shopify", "~> 1.0.4", require: false
8
+ gem "rspec"
9
+ gem "railties", "~> 7.2.0"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rubocop"
7
+ gem "rubocop-shopify", "~> 1.0.4", require: false
8
+ gem "rspec"
9
+ gem "railties", "~> 8.0.0"
10
+
11
+ gemspec path: "../"
@@ -22,7 +22,6 @@ module NexusSemanticLogger
22
22
  hash[:source] = :rails
23
23
  level = hash.delete(:level)
24
24
  hash[:status] = level
25
- hash[:railsenv] = Rails.env
26
25
  hash[:service] = @service
27
26
  hash.delete(:application)
28
27
  hash.delete(:environment)
@@ -18,7 +18,6 @@ module NexusSemanticLogger
18
18
  # Note that 'env' is NOT sent- that is set as the default on the agent e.g. staging, canary, production.
19
19
  # It does not necessarily align with the Rails env, and we do not want to double tag the env.
20
20
  global_tags = [
21
- "railsenv:#{Rails.env}",
22
21
  "service:#{service}",
23
22
  "container_name:#{container_name}",
24
23
  "pod_name:#{pod_name}",
@@ -42,7 +41,6 @@ module NexusSemanticLogger
42
41
  # Trace tags API is Hash<String,String>, see https://www.rubydoc.info/gems/ddtrace/Datadog/Tracing
43
42
  # Should match the global tags, but as a Hash.
44
43
  c.tags = {
45
- railsenv: Rails.env,
46
44
  service: service,
47
45
  container_name: container_name,
48
46
  pod_name: pod_name,
@@ -14,8 +14,19 @@ module ActionDispatch
14
14
  # log_rescued_responses? is a rails7 feature, but this gem is also used on rails6. Check for its existence.
15
15
  return if respond_to?('log_rescued_responses?') && !log_rescued_responses?(request) && wrapper.rescue_response?
16
16
 
17
- ActiveSupport::Deprecation.silence do
18
- ActionController::Base.logger.fatal(wrapper.exception)
17
+ # Silence deprecations emitted while logging the exception. The API changed in Rails 7.1: the
18
+ # ActiveSupport::Deprecation singleton was deprecated and its class-level `silence` was removed in 7.2,
19
+ # replaced by Rails.application.deprecators. Calling the removed singleton on >= 7.2 raises NoMethodError
20
+ # inside render_exception (before the exception is logged), which swallowed all exception logging and
21
+ # produced empty 500s. Use whichever API the Rails version provides, mirroring rails_semantic_logger.
22
+ if (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR > 7
23
+ Rails.application.deprecators.silence do
24
+ ActionController::Base.logger.fatal(wrapper.exception)
25
+ end
26
+ else
27
+ ActiveSupport::Deprecation.silence do
28
+ ActionController::Base.logger.fatal(wrapper.exception)
29
+ end
19
30
  end
20
31
  end
21
32
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+ require 'open3'
3
+ require 'semantic_logger'
4
+
5
+ module NexusSemanticLogger
6
+ # Instrumented replacement for Open3.capture3, so child processes (7z, unrar, yara, ...) are
7
+ # observable. Each call produces:
8
+ # - a Datadog APM span ('shell.exec', resource = binary basename) tagged with the command line,
9
+ # exit status, terminating signal and peak RSS
10
+ # - statsd metrics: nexus.shell.duration_ms (timing), nexus.shell.peak_rss_mb (distribution)
11
+ # and nexus.shell.executions (count), tagged with command:<binary> and
12
+ # outcome:<success|failure|signaled|exception>
13
+ # - an info log line with the same payload
14
+ #
15
+ # Peak RSS is the child's kernel-maintained high-water mark (VmHWM in /proc/<pid>/status),
16
+ # sampled while the child runs. Linux only; nil elsewhere. Direct child only - grandchildren
17
+ # are not measured. A process killed by the kernel OOM killer reports outcome:signaled with
18
+ # termsig 9 and the last-sampled peak RSS, which is exactly the evidence an OOM postmortem needs.
19
+ class TracedShell
20
+ include SemanticLogger::Loggable
21
+
22
+ SPAN_NAME = 'shell.exec'
23
+ METRIC_PREFIX = 'nexus.shell'
24
+ RSS_POLL_INTERVAL_SECONDS = 0.05
25
+ COMMAND_LOG_MAX_CHARS = 500
26
+
27
+ # Drop-in replacement for Open3.capture3.
28
+ # A leading env Hash and trailing spawn options are supported as with Open3; the env Hash is
29
+ # excluded from spans/logs since it may hold secrets.
30
+ # @return [Array(String, String, Process::Status)] stdout, stderr, status.
31
+ def capture3(*cmd, stdin_data: '', binmode: false, **opts)
32
+ binary = command_basename(cmd)
33
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
+ outcome = 'exception'
35
+ status = nil
36
+ peak_rss_kb = nil
37
+ trace(binary, cmd) do |span|
38
+ stdout, stderr, status, peak_rss_kb = run_capture3(cmd, stdin_data, binmode, opts)
39
+ outcome = outcome_for(status)
40
+ annotate_span(span, status, peak_rss_kb)
41
+ [stdout, stderr, status]
42
+ end
43
+ ensure
44
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
45
+ record(cmd, outcome, duration_ms, status, peak_rss_kb)
46
+ end
47
+
48
+ private
49
+
50
+ def trace(binary, cmd, &block)
51
+ return yield(nil) unless defined?(Datadog::Tracing)
52
+
53
+ Datadog::Tracing.trace(SPAN_NAME, resource: binary, type: 'system') do |span|
54
+ span.set_tag('shell.command', command_string(cmd))
55
+ block.call(span)
56
+ end
57
+ end
58
+
59
+ # Open3.capture3 semantics, reimplemented over popen3 so the child pid is available to the
60
+ # RSS poller while it runs.
61
+ def run_capture3(cmd, stdin_data, binmode, opts)
62
+ Open3.popen3(*cmd, **opts) do |stdin, stdout, stderr, wait_thr|
63
+ if binmode
64
+ stdin.binmode
65
+ stdout.binmode
66
+ stderr.binmode
67
+ end
68
+ poller = start_rss_poller(wait_thr)
69
+ out_reader = Thread.new { stdout.read }
70
+ err_reader = Thread.new { stderr.read }
71
+ begin
72
+ stdin.write(stdin_data) if stdin_data && !stdin_data.empty?
73
+ rescue Errno::EPIPE
74
+ # Child exited without consuming stdin; Open3.capture3 ignores this too.
75
+ end
76
+ stdin.close
77
+ status = wait_thr.value
78
+ [out_reader.value, err_reader.value, status, poller&.value]
79
+ end
80
+ end
81
+
82
+ def start_rss_poller(wait_thr)
83
+ return nil unless procfs?
84
+
85
+ pid = wait_thr.pid
86
+ Thread.new do
87
+ peak_kb = nil
88
+ while wait_thr.alive?
89
+ sample = read_vm_hwm_kb(pid)
90
+ peak_kb = sample if sample && (peak_kb.nil? || sample > peak_kb)
91
+ sleep(RSS_POLL_INTERVAL_SECONDS)
92
+ end
93
+ peak_kb
94
+ end
95
+ end
96
+
97
+ # VmHWM is the process's peak RSS as maintained by the kernel, so each sample is itself a
98
+ # high-water mark; the true peak can only be missed by the process exiting within one poll.
99
+ def read_vm_hwm_kb(pid)
100
+ File.read("/proc/#{pid}/status")[/^VmHWM:\s*(\d+)\s*kB/, 1]&.to_i
101
+ rescue SystemCallError, IOError
102
+ nil
103
+ end
104
+
105
+ def procfs?
106
+ return @procfs if defined?(@procfs)
107
+
108
+ @procfs = File.exist?('/proc/self/status')
109
+ end
110
+
111
+ def outcome_for(status)
112
+ return 'success' if status.success?
113
+ return 'signaled' if status.signaled?
114
+
115
+ 'failure'
116
+ end
117
+
118
+ def annotate_span(span, status, peak_rss_kb)
119
+ return unless span
120
+
121
+ span.set_tag('shell.exit_status', status.exitstatus) if status.exitstatus
122
+ span.set_tag('shell.termsig', status.termsig) if status.signaled?
123
+ span.set_tag('shell.peak_rss_mb', to_mb(peak_rss_kb)) if peak_rss_kb
124
+ end
125
+
126
+ def record(cmd, outcome, duration_ms, status, peak_rss_kb)
127
+ tags = ["command:#{command_basename(cmd)}", "outcome:#{outcome}"]
128
+ metrics = NexusSemanticLogger.metrics
129
+ metrics.timing("#{METRIC_PREFIX}.duration_ms", duration_ms, tags: tags)
130
+ metrics.increment("#{METRIC_PREFIX}.executions", tags: tags)
131
+ peak_rss_mb = peak_rss_kb && to_mb(peak_rss_kb)
132
+ metrics.distribution("#{METRIC_PREFIX}.peak_rss_mb", peak_rss_mb, tags: tags) if peak_rss_mb
133
+
134
+ payload = {
135
+ command: command_string(cmd),
136
+ outcome: outcome,
137
+ duration_ms: duration_ms,
138
+ exit_status: status&.exitstatus,
139
+ termsig: status&.signaled? ? status.termsig : nil,
140
+ peak_rss_mb: peak_rss_mb,
141
+ }.compact
142
+ logger.info('Shell command finished.', payload)
143
+ end
144
+
145
+ # Binary basename for span resource and metric tags; deliberately low cardinality.
146
+ def command_basename(cmd)
147
+ first = cmd.first.is_a?(Hash) ? cmd[1] : cmd.first
148
+ first = first.first if first.is_a?(Array) # [binary, argv0] form
149
+ return 'unknown' unless first.is_a?(String)
150
+
151
+ File.basename(first.split(' ').first.to_s)
152
+ end
153
+
154
+ def command_string(cmd)
155
+ parts = cmd.reject { |part| part.is_a?(Hash) }
156
+ parts.flatten.join(' ').slice(0, COMMAND_LOG_MAX_CHARS)
157
+ end
158
+
159
+ def to_mb(kb)
160
+ kb.fdiv(1024).round(1)
161
+ end
162
+ end
163
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module NexusSemanticLogger
3
- # Leave this as 1.14.1 in order for CI process to replace with the tagged version.
4
- VERSION = '1.14.1'
3
+ # Leave this as 1.15.0 in order for CI process to replace with the tagged version.
4
+ VERSION = '1.15.0'
5
5
  end
@@ -5,12 +5,18 @@ require 'nexus_semantic_logger/datadog_formatter'
5
5
  require 'nexus_semantic_logger/datadog_singleton'
6
6
  require 'nexus_semantic_logger/datadog_tracer'
7
7
  require 'nexus_semantic_logger/logger_metrics_subscriber'
8
+ require 'nexus_semantic_logger/traced_shell'
8
9
 
9
10
  module NexusSemanticLogger
10
11
  # Get application wide object for sending metrics.
11
12
  def self.metrics
12
13
  DatadogSingleton.instance
13
14
  end
15
+
16
+ # Get application wide object for running instrumented shell commands.
17
+ def self.shell
18
+ @shell ||= TracedShell.new
19
+ end
14
20
  end
15
21
 
16
22
  # Patch access to LEVELS array.
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nexus_semantic_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.14.1
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johnathon Harris
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-07-07 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: amazing_print
@@ -107,16 +108,22 @@ dependencies:
107
108
  - - "~>"
108
109
  - !ruby/object:Gem::Version
109
110
  version: 4.16.1
111
+ description:
110
112
  email: john.harris@nexusmods.com
111
113
  executables: []
112
114
  extensions: []
113
115
  extra_rdoc_files: []
114
116
  files:
117
+ - ".github/workflows/ci.yml"
115
118
  - ".gitignore"
116
- - ".gitlab-ci.yml"
119
+ - ".rspec"
117
120
  - ".rubocop.yml"
121
+ - Appraisals
118
122
  - Gemfile
119
123
  - README.md
124
+ - gemfiles/rails_7_1.gemfile
125
+ - gemfiles/rails_7_2.gemfile
126
+ - gemfiles/rails_8_0.gemfile
120
127
  - lib/nexus_semantic_logger.rb
121
128
  - lib/nexus_semantic_logger/appender_filter.rb
122
129
  - lib/nexus_semantic_logger/application.rb
@@ -126,12 +133,15 @@ files:
126
133
  - lib/nexus_semantic_logger/extensions/action_dispatch/debug_exceptions.rb
127
134
  - lib/nexus_semantic_logger/logger_metrics_subscriber.rb
128
135
  - lib/nexus_semantic_logger/sneakers_metrics.rb
136
+ - lib/nexus_semantic_logger/traced_shell.rb
129
137
  - lib/nexus_semantic_logger/version.rb
130
138
  - lib/puma/plugin/README.md
131
139
  - lib/puma/plugin/nexus_puma_statsd.rb
132
140
  - nexus_semantic_logger.gemspec
141
+ homepage:
133
142
  licenses: []
134
143
  metadata: {}
144
+ post_install_message:
135
145
  rdoc_options: []
136
146
  require_paths:
137
147
  - lib
@@ -146,7 +156,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
156
  - !ruby/object:Gem::Version
147
157
  version: '0'
148
158
  requirements: []
149
- rubygems_version: 4.0.3
159
+ rubygems_version: 3.4.10
160
+ signing_key:
150
161
  specification_version: 4
151
162
  summary: semantic_logger usage for nexus
152
163
  test_files: []
data/.gitlab-ci.yml DELETED
@@ -1,36 +0,0 @@
1
- image: "ruby:3.2.2"
2
-
3
- stages:
4
- - release
5
- - test
6
-
7
- before_script:
8
- - gem install bundler --no-document
9
- - bundle install --jobs $(nproc) "${FLAGS[@]}"
10
-
11
- rspec:
12
- script:
13
- - bundle exec rspec
14
-
15
- rubocop:
16
- script:
17
- - bundle exec rubocop
18
-
19
- release:
20
- stage: release
21
- rules:
22
- - if: '$CI_COMMIT_TAG'
23
- script:
24
- - mkdir -p ~/.gem
25
- - cp $RUBYGEMS_CREDENTIALS ~/.gem/credentials
26
- - chmod 0600 ~/.gem/credentials
27
- - gem update --system
28
- - ruby --version
29
- - gem env version
30
- - sed -i "s/0.0.0/$CI_COMMIT_TAG/g" lib/nexus_semantic_logger/version.rb
31
- - gem build nexus_semantic_logger.gemspec
32
- - gem push nexus_semantic_logger*.gem
33
- artifacts:
34
- paths:
35
- - nexus_semantic_logger*.gem
36
- expire_in: 30 days