Module: PostgresExtension

Defined in:
lib/instrumentation/postgres.rb

Constant Summary collapse

SQL_COMMANDS =

A list of SQL commands, from: www.postgresql.org/docs/current/sql-commands.html Commands are truncated to their first word, and all duplicates are removed, This favors brevity and low-cardinality over descriptiveness.

%w[
  ABORT
  ALTER
  ANALYZE
  BEGIN
  CALL
  CHECKPOINT
  CLOSE
  CLUSTER
  COMMENT
  COMMIT
  COPY
  CREATE
  DEALLOCATE
  DECLARE
  DELETE
  DISCARD
  DO
  DROP
  END
  EXECUTE
  EXPLAIN
  FETCH
  GRANT
  IMPORT
  INSERT
  LISTEN
  LOAD
  LOCK
  MOVE
  NOTIFY
  PREPARE
  PREPARE
  REASSIGN
  REFRESH
  REINDEX
  RELEASE
  RESET
  REVOKE
  ROLLBACK
  SAVEPOINT
  SECURITY
  SELECT
  SELECT
  SET
  SHOW
  START
  TRUNCATE
  UNLISTEN
  UPDATE
  VACUUM
  VALUES
].freeze
COMPONENTS_REGEX_MAP =
{
  single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
  dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
  uuids: /\{?(?:[0-9a-fA-F]\-*){32}\}?/,
  numeric_literals: /-?\b(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/,
  boolean_literals: /\b(?:true|false|null)\b/i,
  comments: /(?:#|--).*?(?=\r|\n|$)/i,
  multi_line_comments: %r{\/\*(?:[^\/]|\/[^*])*?(?:\*\/|\/\*.*)}
}.freeze
POSTGRES_COMPONENTS =
%i[
  single_quotes
  dollar_quotes
  uuids
  numeric_literals
  boolean_literals
  comments
  multi_line_comments
].freeze
UNMATCHED_PAIRS_REGEX =
%r{'|\/\*|\*\/|\$(?!\?)}.freeze
EXEC_ISH_METHODS =

These are all alike in that they will have a SQL statement as the first parameter. That statement may possibly be parameterized, but we can still use it - the obfuscation code will just transform $1 -> $? in that case (which is fine enough).

%i[
  exec
  query
  sync_exec
  async_exec
  exec_params
  async_exec_params
  sync_exec_params
].freeze
PREPARE_ISH_METHODS =

The following methods all take a statement name as the first parameter, and a SQL statement as the second - and possibly further parameters after that. We can trace them all alike.

%i[
  prepare
  async_prepare
  sync_prepare
].freeze
EXEC_PREPARED_ISH_METHODS =

The following methods take a prepared statement name as their first parameter - everything after that is either potentially quite sensitive (an array of bind params) or not useful to us. We trace them all alike.

%i[
  exec_prepared
  async_exec_prepared
  sync_exec_prepared
].freeze

Instance Method Summary collapse

Instance Method Details

#client_attributesObject



221
222
223
224
225
226
227
228
229
230
231
# File 'lib/instrumentation/postgres.rb', line 221

def client_attributes
  attributes = {
    'db.system' => 'postgresql',
    'db.user' => conninfo_hash[:user]&.to_s,
    'db.name' => database_name,
    'net.peer.name' => conninfo_hash[:host]&.to_s
  }
  # attributes['peer.service'] = config[:peer_service] # if config[:peer_service]

  attributes.merge(transport_attrs).reject { |_, v| v.nil? }
end

#configObject



142
143
144
# File 'lib/instrumentation/postgres.rb', line 142

def config
  EpsagonPostgresInstrumentation.instance.config
end

#database_nameObject



217
218
219
# File 'lib/instrumentation/postgres.rb', line 217

def database_name
  conninfo_hash[:dbname]&.to_s
end

#extract_operation(sql) ⇒ Object



208
209
210
211
# File 'lib/instrumentation/postgres.rb', line 208

def extract_operation(sql)
  # From: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/9244a08a8d014afe26b82b91cf86e407c2599d73/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts#L35
  sql.to_s.split[0].to_s.upcase
end

#generated_postgres_regexObject



213
214
215
# File 'lib/instrumentation/postgres.rb', line 213

def generated_postgres_regex
  @generated_postgres_regex ||= Regexp.union(PostgresExtension::POSTGRES_COMPONENTS.map { |component| PostgresExtension::COMPONENTS_REGEX_MAP[component] })
end

#lru_cacheObject



150
151
152
153
154
155
156
157
158
159
160
# File 'lib/instrumentation/postgres.rb', line 150

def lru_cache
  # When SQL is being sanitized, we know that this cache will
  # never be more than 50 entries * 2000 characters (so, presumably
  # 100k bytes - or 97k). When not sanitizing SQL, then this cache
  # could grow much larger - but the small cache size should otherwise
  # help contain memory growth. The intended use here is to cache
  # prepared SQL statements, so that we can attach a reasonable
  # `db.sql.statement` value to spans when those prepared statements
  # are executed later on.
  @lru_cache ||= LruCache.new(50)
end

#span_attrs(kind, *args) ⇒ Object

Rubocop is complaining about 19.31/18 for Metrics/AbcSize. But, getting that metric in line would force us over the module size limit! We can’t win here unless we want to start abstracting things into a million pieces.



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/instrumentation/postgres.rb', line 166

def span_attrs(kind, *args) # rubocop:disable Metrics/AbcSize
  if kind == :query
    operation = extract_operation(args[0])
    sql = args[0]
  else
    statement_name = args[0]

    if kind == :prepare
      sql = args[1]
      lru_cache[statement_name] = sql
      operation = 'PREPARE'
    else
      sql = lru_cache[statement_name]
      operation = 'EXECUTE'
    end
  end

  attrs = { 'db.operation' => validated_operation(operation), 'db.postgresql.prepared_statement_name' => statement_name }
  attrs['db.statement'] = sql if config[:epsagon][:metadata_only] == false
  attrs['db.sql.table'] = table_name(sql)
  attrs.reject! { |_, v| v.nil? }

  [database_name, client_attributes.merge(attrs)]
end

#table_name(sql) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/instrumentation/postgres.rb', line 191

def table_name(sql)
  return '' if sql.nil?

  parsed_query = PgQuery.parse(sql)
  if parsed_query.tables.length == 0
    ''
  else
    parsed_query.tables[0]
  end
rescue PgQuery::ParseError
  ''
end

#tracerObject



146
147
148
# File 'lib/instrumentation/postgres.rb', line 146

def tracer
  EpsagonPostgresInstrumentation.instance.tracer
end

#transport_attrsObject



233
234
235
236
237
238
239
240
241
242
243
# File 'lib/instrumentation/postgres.rb', line 233

def transport_attrs
  if conninfo_hash[:host]&.start_with?('/')
    { 'net.transport' => 'Unix' }
  else
    {
      'net.transport' => 'IP.TCP',
      'net.peer.ip' => conninfo_hash[:hostaddr]&.to_s,
      'net.peer.port' => conninfo_hash[:port]&.to_s
    }
  end
end

#validated_operation(operation) ⇒ Object



204
205
206
# File 'lib/instrumentation/postgres.rb', line 204

def validated_operation(operation)
  operation if PostgresExtension::SQL_COMMANDS.include?(operation)
end