# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# frozen_string_literal: true
require 'erb'
require 'http'
require 'json'
require 'yaml'
require 'zlib'
require 'logger'
require 'concurrent'
require 'forwardable'
require 'securerandom'
require 'elastic_apm/version'
require 'elastic_apm/internal_error'
require 'elastic_apm/logging'
# Core
require 'elastic_apm/fields'
require 'elastic_apm/agent'
require 'elastic_apm/config'
require 'elastic_apm/context'
require 'elastic_apm/instrumenter'
require 'elastic_apm/util'
require 'elastic_apm/middleware'
require 'elastic_apm/graphql'
require 'elastic_apm/rails' if defined?(::Rails::Railtie)
require 'elastic_apm/sinatra' if defined?(::Sinatra)
require 'elastic_apm/grape' if defined?(::Grape)
require 'elastic_apm/grpc' if defined?(::GRPC)
# ElasticAPM
module ElasticAPM
class << self
### Life cycle
# Starts the ElasticAPM Agent
#
# @param config [Config] An instance of Config
# @return [Agent] The resulting [Agent]
def start(config = {})
Agent.start config
end
# Stops the ElasticAPM Agent
def stop
Agent.stop
end
# Restarts the ElasticAPM Agent using the same config or a new
# one, if it is provided.
# Starts the agent if it is not running.
# Stops and starts the agent if it is running.
def restart(config = nil)
config ||= agent&.config
stop if running?
start(config)
end
# @return [Boolean] Whether there's an [Agent] running
def running?
Agent.running?
end
# @return [Agent] Currently running [Agent] if any
def agent
Agent.instance
end
### Metrics
# Returns the currently active transaction (if any)
#
# @return [Transaction] or `nil`
def current_transaction
agent&.current_transaction
end
# Returns the currently active span (if any)
#
# @return [Span] or `nil`
def current_span
agent&.current_span
end
# Get a formatted string containing transaction, span, and trace ids.
# If a block is provided, the ids are yielded.
#
# @yield [String|nil, String|nil, String|nil] The transaction, span,
# and trace ids.
# @return [String] Unless block given
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def log_ids
trace_id = (current_transaction || current_span)&.trace_id
if block_given?
return yield(current_transaction&.id, current_span&.id, trace_id)
end
ids = []
ids << "transaction.id=#{current_transaction.id}" if current_transaction
ids << "span.id=#{current_span.id}" if current_span
ids << "trace.id=#{trace_id}" if trace_id
ids.join(' ')
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# Start a new transaction
#
# @param name [String] A description of the transaction, eg
# `ExamplesController#index`
# @param type [String] The kind of the transaction, eg `app.request.get` or
# `db.mysql2.query`
# @param context [Context] An optional [Context]
# @param trace_context [TraceContext] An optional [TraceContext] object for
# Distributed Tracing.
# @return [Transaction]
def start_transaction(
name = nil,
type = nil,
context: nil,
trace_context: nil
)
agent&.start_transaction(
name,
type,
context: context,
trace_context: trace_context
)
end
# Ends the current transaction with `result`
#
# @param result [String] The result of the transaction
# @return [Transaction]
def end_transaction(result = nil)
agent&.end_transaction(result)
end
# Wrap a block in a Transaction, ending it after the block
#
# @param name [String] A description of the transaction, eg
# `ExamplesController#index`
# @param type [String] The kind of the transaction, eg `app.request.get` or
# `db.mysql2.query`
# @param context [Context] An optional [Context]
# @param trace_context [TraceContext] An optional [TraceContext] object for
# Distributed Tracing.
# @yield [Transaction]
# @return result of block
def with_transaction(
name = nil,
type = nil,
context: nil,
trace_context: nil
)
unless block_given?
raise ArgumentError,
'expected a block. Do you want `start_transaction\' instead?'
end
return yield(nil) unless agent
begin
transaction =
start_transaction(
name,
type,
context: context,
trace_context: trace_context
)
result = yield transaction
transaction&.outcome ||= Transaction::Outcome::SUCCESS
result
rescue
transaction&.outcome ||= Transaction::Outcome::FAILURE
raise
ensure
end_transaction
end
end
# rubocop:disable Metrics/ParameterLists
# Start a new span
#
# @param name [String] A description of the span, eq `SELECT FROM "users"`
# @param type [String] The span type, eq `db`
# @param subtype [String] The span subtype, eq `postgresql`
# @param action [String] The span action type, eq `connect` or `query`
# @param context [Span::Context] Context information about the span
# @param include_stacktrace [Boolean] Whether or not to capture a stacktrace
# @param trace_context [TraceContext] An optional [TraceContext] object for
# Distributed Tracing.
# @param parent [Transaction,Span] The parent transaction or span.
# Relevant when the span is created in another thread.
# @param sync [Boolean] Whether the span is created synchronously or not.
# @return [Span]
def start_span(
name,
type = nil,
subtype: nil,
action: nil,
context: nil,
include_stacktrace: true,
trace_context: nil,
parent: nil,
sync: nil
)
agent&.start_span(
name,
type,
subtype: subtype,
action: action,
context: context,
trace_context: trace_context,
parent: parent,
sync: sync
).tap do |span|
break unless span && include_stacktrace
break unless agent.config.span_frames_min_duration?
span.original_backtrace ||= caller
end
end
# rubocop:enable Metrics/ParameterLists
# Ends the current span
#
# @return [Span]
def end_span
agent&.end_span
end
# rubocop:disable Metrics/ParameterLists
# Wrap a block in a Span, ending it after the block
#
# @param name [String] A description of the span, eq `SELECT FROM "users"`
# @param type [String] The kind of span, eq `db`
# @param subtype [String] The subtype of span eg. `postgresql`.
# @param action [String] The action type of span eg. `connect` or `query`.
# @param context [Span::Context] Context information about the span
# @param include_stacktrace [Boolean] Whether or not to capture a stacktrace
# @param trace_context [TraceContext] An optional [TraceContext] object for
# Distributed Tracing.
# @param parent [Transaction,Span] The parent transaction or span.
# Relevant when the span is created in another thread.
# @param sync [Boolean] Whether the span is created synchronously or not.
# @yield [Span]
# @return Result of block
def with_span(
name,
type = nil,
subtype: nil,
action: nil,
context: nil,
include_stacktrace: true,
trace_context: nil,
parent: nil,
sync: nil
)
unless block_given?
raise ArgumentError,
'expected a block. Do you want `start_span\' instead?'
end
return yield nil unless agent
begin
span =
start_span(
name,
type,
subtype: subtype,
action: action,
context: context,
include_stacktrace: include_stacktrace,
trace_context: trace_context,
parent: parent,
sync: sync
)
result = yield span
span&.outcome ||= Span::Outcome::SUCCESS
result
rescue
span&.outcome ||= Span::Outcome::FAILURE
raise
ensure
end_span
end
end
# rubocop:enable Metrics/ParameterLists
# Build a [Context] from a Rack `env`. The context may include information
# about the request, response, current user and more
#
# @param rack_env [Rack::Env] A Rack env
# @return [Context] The built context
def build_context(
rack_env: nil,
for_type: :transaction
)
agent&.build_context(rack_env: rack_env, for_type: for_type)
end
### Errors
# Report and exception to APM
#
# @param exception [Exception] The exception
# @param context [Context] An optional [Context]
# @param handled [Boolean] Whether the exception was rescued
# @return [String] ID of the generated [Error]
def report(exception, context: nil, handled: true)
agent&.report(exception, context: context, handled: handled)
end
# Report a custom string error message to APM
#
# @param message [String] The message
# @param context [Context] An optional [Context]
# @return [String] ID of the generated [Error]
def report_message(message, context: nil, **attrs)
agent&.report_message(
message,
context: context,
backtrace: caller,
**attrs
)
end
### Context
# Set a _label_ value for the current transaction
#
# @param key [String,Symbol] A key
# @param value [Object] A value
# @return [Object] The given value
def set_label(key, value)
case value
when TrueClass,
FalseClass,
Numeric,
NilClass,
String
agent&.set_label(key, value)
else
agent&.set_label(key, value.to_s)
end
end
# Provide further context for the current transaction
#
# @param custom [Hash] A hash with custom information. Can be nested.
# @return [Hash] The current custom context
def set_custom_context(custom)
agent&.set_custom_context(custom)
end
# Provide a user to the current transaction
#
# @param user [Object] An object representing a user
# @return [Object] Given user
def set_user(user)
agent&.set_user(user)
end
# Set destination fields on the current span
#
# @param address [String] Destination address
# @param address [String] Destination address
# @param address [Hash] Destination service
# @param address [Hash] Destination cloud
def set_destination(address: nil, port: nil, service: nil, cloud: nil)
agent&.set_destination(address: address, port: port, service: service, cloud: cloud)
end
# Provide a filter to transform payloads before sending them off
#
# @param key [Symbol] Unique filter key
# @param callback [Object, Proc] A filter that responds to #call(payload)
# @yield [Hash] A filter. Used if provided. Otherwise using `callback`
# @return [Bool] true
def add_filter(key, callback = nil, &block)
if callback.nil? && !block
raise ArgumentError, '#add_filter needs either `callback\' or a block'
end
agent&.add_filter(key, block || callback)
end
end
end