nexus_semantic_logger 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/README.md +14 -0
- data/lib/nexus_semantic_logger/datadog_formatter.rb +0 -1
- data/lib/nexus_semantic_logger/datadog_tracer.rb +0 -2
- data/lib/nexus_semantic_logger/traced_shell.rb +163 -0
- data/lib/nexus_semantic_logger/version.rb +2 -2
- data/lib/nexus_semantic_logger.rb +6 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0dcbcb0a7a4c7de68e7496b8ffb7f6d5d5e08742a981af559e48162bd149fc40
|
|
4
|
+
data.tar.gz: 4cd8249c19ce91fc5a3f2ef7c1919f91685235cf385291f8e0272ecfaf71234f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17a71300ae2d6f1bd8c5d0175b167071b818c54ea489d5973a6a2d92cc2fa99dd8f8369015b57520ae8eaf2dfef84f17b30e28c12736637c1620d48ec435d28c
|
|
7
|
+
data.tar.gz: f93a642bc2e30c4913f65cab35cbda108a99d9b15f78fab5f715aa91d53511560d9a2189bd10d02782a90c9b810add8efec1509278bb6108fd4a1ee0b9258416
|
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:
|
|
@@ -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,
|
|
@@ -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.
|
|
4
|
-
VERSION = '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,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nexus_semantic_logger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johnathon Harris
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: amazing_print
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- lib/nexus_semantic_logger/extensions/action_dispatch/debug_exceptions.rb
|
|
134
134
|
- lib/nexus_semantic_logger/logger_metrics_subscriber.rb
|
|
135
135
|
- lib/nexus_semantic_logger/sneakers_metrics.rb
|
|
136
|
+
- lib/nexus_semantic_logger/traced_shell.rb
|
|
136
137
|
- lib/nexus_semantic_logger/version.rb
|
|
137
138
|
- lib/puma/plugin/README.md
|
|
138
139
|
- lib/puma/plugin/nexus_puma_statsd.rb
|