forge_ops_tracker 0.14.0 → 0.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/CHANGELOG.md +8 -0
- data/README.md +6 -3
- data/lib/forge_ops_tracker/performance_instrumentation.rb +3 -2
- data/lib/forge_ops_tracker/query_explainer.rb +3 -2
- data/lib/forge_ops_tracker/sql_statement.rb +18 -7
- data/lib/forge_ops_tracker/version.rb +1 -1
- data/lib/forge_ops_tracker.rb +3 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c05c6f94c7c080f7c148cc315097b8e177d34dec772d580e21cf89a7120f1b3
|
|
4
|
+
data.tar.gz: d486796b0f26b752872118cfaad2c9c184671bc596bd6c32a04cfb6a754dc171
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e99dddf991705420ceb4d69f21ad8d5e39885f5f2f782e0747976aa2c8cb2463a4d8f56652f23f6944c40d38bc89e95475e61ecaf2079b0f106ca401d4e25d06
|
|
7
|
+
data.tar.gz: 1e5e0e69a95ddecb9411a024dcbf1817b688312131bbffc7359cb917f86d416e7d2811b1215feeb355b4ff4df3b1a2757bafe747ddc425e057e3248bb3572287
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
- SQL masking now also catches strings with a backslash-escaped quote (`'o\'brien'`,
|
|
6
|
+
`E'o\'brien'`), prefixed strings (`E''`, `X''`, `N''`, `B''`, `U&''`, prefix included), hex,
|
|
7
|
+
binary and exponent numbers (`0x1F`, `0b101`, `3e10`, `1.5E-3`, `.5`), and, on MySQL and
|
|
8
|
+
MariaDB, `"double-quoted"` strings. `SqlStatement.mask` takes an optional `system:`, and
|
|
9
|
+
database spans (including `ForgeOpsTracker.span`'s `db_system:`) pass their `db.system` to it.
|
|
10
|
+
|
|
3
11
|
## 0.14.0 (2026-09-25)
|
|
4
12
|
|
|
5
13
|
- Every Active Record database span now carries the query's SQL (`db.statement`), masked so every
|
data/README.md
CHANGED
|
@@ -416,9 +416,12 @@ on error events is sent either way.
|
|
|
416
416
|
Every Active Record query span carries the query's SQL and the database it ran on, so a slow trace
|
|
417
417
|
shows exactly which query was slow. The SQL is masked before it leaves your process: every string
|
|
418
418
|
and number is replaced by `?`, so `WHERE email = 'a@b.co' AND id = 42` is sent as
|
|
419
|
-
`WHERE email = ? AND id = ?`, and bind values are never sent at all.
|
|
420
|
-
|
|
421
|
-
`
|
|
419
|
+
`WHERE email = ? AND id = ?`, and bind values are never sent at all. That includes escaped and
|
|
420
|
+
prefixed strings (`E'o\'brien'`, `X'DEADBEEF'`), hex, binary and exponent numbers (`0x1F`,
|
|
421
|
+
`0b101`, `1.5E-3`), and, on MySQL and MariaDB (where `"double quotes"` hold a string, not a name),
|
|
422
|
+
double-quoted strings. ForgeOps masks it again on arrival. The database comes from the
|
|
423
|
+
connection's adapter (`postgresql`, `mysql`, `sqlite`, `mssql`, `oracle`, or the adapter's own
|
|
424
|
+
name otherwise).
|
|
422
425
|
|
|
423
426
|
Spans you create yourself can carry SQL too. Pass `statement:` (and optionally `db_system:`) on a
|
|
424
427
|
`kind: "database"` span; it's masked the same way:
|
|
@@ -84,9 +84,10 @@ module ForgeOpsTracker
|
|
|
84
84
|
# and number literal becomes "?", so no value the app queried with is ever sent), and which
|
|
85
85
|
# database it ran on, from the connection's adapter. Either is left out when it isn't known.
|
|
86
86
|
def database_span_data(payload)
|
|
87
|
+
system = db_system(payload[:connection])
|
|
87
88
|
{
|
|
88
|
-
"db.statement" => SqlStatement.mask(payload[:sql].to_s),
|
|
89
|
-
"db.system" =>
|
|
89
|
+
"db.statement" => SqlStatement.mask(payload[:sql].to_s, system: system),
|
|
90
|
+
"db.system" => system
|
|
90
91
|
}.compact
|
|
91
92
|
end
|
|
92
93
|
|
|
@@ -102,7 +102,8 @@ module ForgeOpsTracker
|
|
|
102
102
|
return false unless duration_ms && duration_ms >= configuration.explain_threshold_ms.to_f
|
|
103
103
|
|
|
104
104
|
connection = payload[:connection]
|
|
105
|
-
|
|
105
|
+
system = PerformanceInstrumentation.db_system(connection)
|
|
106
|
+
return false unless system == "postgresql"
|
|
106
107
|
|
|
107
108
|
sql = payload[:sql]
|
|
108
109
|
return false unless self.class.explainable?(sql)
|
|
@@ -110,7 +111,7 @@ module ForgeOpsTracker
|
|
|
110
111
|
pool = connection.respond_to?(:pool) ? connection.pool : nil
|
|
111
112
|
return false unless pool
|
|
112
113
|
|
|
113
|
-
statement = SqlStatement.mask(sql)
|
|
114
|
+
statement = SqlStatement.mask(sql, system: system)
|
|
114
115
|
return false unless reserve(statement)
|
|
115
116
|
|
|
116
117
|
enqueue(Job.new(
|
|
@@ -15,11 +15,19 @@ module ForgeOpsTracker
|
|
|
15
15
|
MAX_NAME_LENGTH = 200
|
|
16
16
|
MAX_CAUSE_DEPTH = 5
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
# db.system values where "double quotes" are a string, not a name.
|
|
19
|
+
DOUBLE_QUOTED_STRING_SYSTEMS = %w[mysql mariadb].freeze
|
|
20
|
+
|
|
21
|
+
# The prefix only counts as one when it isn't the end of a word, but the quote always does:
|
|
22
|
+
# LIKE'%x%', with no space, is still a string. A backslash can be the statement's last character
|
|
23
|
+
# ('secret\ cut off mid-escape), and the string is still masked to the end.
|
|
24
|
+
STRING = /(?:(?<![\w$])(?:[EeXxNnBb]|[Uu]&))?'(?:[^'\\]|\\(?:.|\z)|'')*(?:'|\z)/m
|
|
25
|
+
DOUBLE_QUOTED_STRING = /"(?:[^"\\]|\\(?:.|\z)|"")*(?:"|\z)/m
|
|
26
|
+
DOLLAR_QUOTED = /(?<tag>\$[A-Za-z_]*\$).*?(?:\k<tag>|\z)/m
|
|
27
|
+
NUMBER = /(?<![\w$.])(?:0[xX]\h+|0[bB][01]+|(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)(?!\w)/
|
|
28
|
+
|
|
29
|
+
LITERAL = Regexp.union(STRING, DOLLAR_QUOTED, NUMBER)
|
|
30
|
+
LITERAL_WITH_DOUBLE_QUOTES = Regexp.union(STRING, DOUBLE_QUOTED_STRING, DOLLAR_QUOTED, NUMBER)
|
|
23
31
|
|
|
24
32
|
PART = /(?:[\w$#@]+|"[^"]+"|\[[^\]]+\]|`[^`]+`)/
|
|
25
33
|
NAME = /#{PART}(?:\.#{PART})*/
|
|
@@ -51,10 +59,13 @@ module ForgeOpsTracker
|
|
|
51
59
|
nil
|
|
52
60
|
end
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
# system is the query's db.system, when known: on MySQL and MariaDB "double quotes" are a
|
|
63
|
+
# string too, so they're masked there and left alone everywhere else.
|
|
64
|
+
def mask(statement, system: nil)
|
|
55
65
|
return nil if statement.nil? || statement.strip.empty?
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
pattern = DOUBLE_QUOTED_STRING_SYSTEMS.include?(system.to_s.downcase) ? LITERAL_WITH_DOUBLE_QUOTES : LITERAL
|
|
68
|
+
masked = statement.gsub(pattern, MASK)
|
|
58
69
|
masked.length > MAX_LENGTH ? "#{masked[0, MAX_LENGTH]}..." : masked
|
|
59
70
|
end
|
|
60
71
|
|
data/lib/forge_ops_tracker.rb
CHANGED
|
@@ -155,9 +155,10 @@ module ForgeOpsTracker
|
|
|
155
155
|
|
|
156
156
|
private
|
|
157
157
|
def database_span_data(data, statement, db_system)
|
|
158
|
+
system = (db_system.to_s.strip.downcase unless db_system.nil? || db_system.to_s.strip.empty?)
|
|
158
159
|
extra = {
|
|
159
|
-
"db.statement" => (SqlStatement.mask(statement.to_s) unless statement.nil?),
|
|
160
|
-
"db.system" =>
|
|
160
|
+
"db.statement" => (SqlStatement.mask(statement.to_s, system: system) unless statement.nil?),
|
|
161
|
+
"db.system" => system
|
|
161
162
|
}.compact
|
|
162
163
|
extra.empty? ? data : data.merge(extra)
|
|
163
164
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forge_ops_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ForgeOps
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '8.1'
|
|
33
33
|
type: :development
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '8.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: puma
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|