newrelic-telemetry_sdk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/.github/ISSUE_TEMPLATE/enhancement.md +27 -0
- data/.github/workflows/assignproj.yml +21 -0
- data/.github/workflows/ci.yml +24 -0
- data/.github/workflows/release.yml +46 -0
- data/.github/workflows/scripts/rubygems-authenticate.py +13 -0
- data/.github/workflows/scripts/rubygems-publish.rb +32 -0
- data/.github/workflows/snyk.yml +26 -0
- data/.gitignore +29 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +10 -0
- data/CODE_OF_CONDUCT.md +5 -0
- data/CONTRIBUTING.md +30 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +95 -0
- data/Rakefile +10 -0
- data/cla.md +16 -0
- data/examples/external_request/Gemfile +3 -0
- data/examples/external_request/simple.rb +80 -0
- data/examples/external_request/with_harvester.rb +89 -0
- data/lib/newrelic/telemetry_sdk.rb +38 -0
- data/lib/newrelic/telemetry_sdk/buffer.rb +65 -0
- data/lib/newrelic/telemetry_sdk/clients/client.rb +262 -0
- data/lib/newrelic/telemetry_sdk/clients/trace_client.rb +43 -0
- data/lib/newrelic/telemetry_sdk/config.rb +97 -0
- data/lib/newrelic/telemetry_sdk/configurator.rb +95 -0
- data/lib/newrelic/telemetry_sdk/exception.rb +14 -0
- data/lib/newrelic/telemetry_sdk/harvester.rb +117 -0
- data/lib/newrelic/telemetry_sdk/logger.rb +90 -0
- data/lib/newrelic/telemetry_sdk/span.rb +97 -0
- data/lib/newrelic/telemetry_sdk/util.rb +37 -0
- data/lib/newrelic/telemetry_sdk/version.rb +16 -0
- data/newrelic-telemetry_sdk.gemspec +37 -0
- metadata +182 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# This file is distributed under New Relic's license terms.
|
4
|
+
# See https://github.com/newrelic/newrelic-telemetry-sdk-ruby/blob/main/LICENSE for complete details.
|
5
|
+
|
6
|
+
require_relative 'client'
|
7
|
+
|
8
|
+
module NewRelic
|
9
|
+
module TelemetrySdk
|
10
|
+
# The {TraceClient} sends {Span} data to the Trace API host endpoint.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# trace_client = NewRelic::TelemetrySdk::TraceClient.new
|
14
|
+
#
|
15
|
+
# span = NewRelic::TelemetrySdk::Span.new(
|
16
|
+
# id: random_id(16),
|
17
|
+
# trace_id: random_id(32),
|
18
|
+
# start_time: Time.now,
|
19
|
+
# name: "Net::HTTP#get"
|
20
|
+
# )
|
21
|
+
# trace_client.report span
|
22
|
+
#
|
23
|
+
class TraceClient < Client
|
24
|
+
def initialize host: trace_api_host
|
25
|
+
super host: host,
|
26
|
+
path: '/trace/v1',
|
27
|
+
headers: {
|
28
|
+
:'Content-Type' => 'application/json',
|
29
|
+
:'Api-Key' => api_insert_key,
|
30
|
+
:'Data-Format' => 'newrelic',
|
31
|
+
:'Data-Format-Version' => '1'
|
32
|
+
},
|
33
|
+
payload_type: :spans
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def trace_api_host
|
39
|
+
TelemetrySdk.config.trace_api_host
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# This file is distributed under New Relic's license terms.
|
4
|
+
# See https://github.com/newrelic/newrelic-telemetry-sdk-ruby/blob/main/LICENSE for complete details.
|
5
|
+
|
6
|
+
module NewRelic
|
7
|
+
module TelemetrySdk
|
8
|
+
|
9
|
+
# This class allows setting configuration options for the Telemetry SDK
|
10
|
+
# via NewRelic::TelemetrySdk.configure.
|
11
|
+
#
|
12
|
+
# @example Setting the API Key
|
13
|
+
# NewRelic::TelemetrySdk.configure do |config|
|
14
|
+
# config.api_insert_key = ENV["API_KEY"]
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
class Config
|
19
|
+
|
20
|
+
# Default host for the Trace Client
|
21
|
+
DEFAULT_TRACE_API_HOST = "https://trace-api.newrelic.com"
|
22
|
+
|
23
|
+
# Default harvest interval in seconds
|
24
|
+
DEFAULT_HARVEST_INTERVAL = 5
|
25
|
+
|
26
|
+
# Default backoff strategy factorial
|
27
|
+
DEFAULT_BACKOFF_FACTOR = 5
|
28
|
+
|
29
|
+
# Default maximum backoff wait time in seconds
|
30
|
+
DEFAULT_BACKOFF_MAX = 80
|
31
|
+
|
32
|
+
# Default number of retries to send same data
|
33
|
+
DEFAULT_MAX_RETRIES = 8
|
34
|
+
|
35
|
+
# @!attribute api_insert_key [optional, String]
|
36
|
+
# A New Relic Insert API key. Necessary for sending data to
|
37
|
+
# New Relic via the Telemetry SDK. Defaults to +API_INSERT_KEY+
|
38
|
+
# environment variable.
|
39
|
+
# @see https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#event-insert-key Types of New Relic API keys: Insert API key
|
40
|
+
attr_accessor :api_insert_key
|
41
|
+
|
42
|
+
# @!attribute logger [optional, Logger]
|
43
|
+
# A Logger object customized with your preferred log output
|
44
|
+
# destination (if any) and log level.
|
45
|
+
attr_accessor :logger
|
46
|
+
|
47
|
+
# @!attribute audit_logging_enabled [optional, Boolean]
|
48
|
+
# If audit logging is enabled, the contents of every payload
|
49
|
+
# sent to New Relic will be recorded in logs.
|
50
|
+
# This is a very verbose log level for debugging purposes.
|
51
|
+
attr_accessor :audit_logging_enabled
|
52
|
+
|
53
|
+
# @!attribute harvest_interval [optional, Integer]
|
54
|
+
# The frequency of automatic harvest (in seconds) if sending data
|
55
|
+
# with a harvester.
|
56
|
+
# Defaults to {DEFAULT_HARVEST_INTERVAL} seconds.
|
57
|
+
attr_accessor :harvest_interval
|
58
|
+
|
59
|
+
# @!attribute backoff_factor [optional, Integer]
|
60
|
+
# The amount of time (in seconds) to wait after sending data
|
61
|
+
# to New Relic fails before attempting to send again.
|
62
|
+
# Defaults to {DEFAULT_HARVEST_INTERVAL} seconds.
|
63
|
+
attr_accessor :backoff_factor
|
64
|
+
|
65
|
+
# @!attribute backoff_max [optional, Integer]
|
66
|
+
# If data cannot be sent to New Relic intermittently, the SDK will
|
67
|
+
# retry the request at increasing intervals, but will stop increasing
|
68
|
+
# the retry intervals when they have reached +backoff_max+ seconds.
|
69
|
+
# Defaults to {DEFAULT_BACKOFF_MAX} seconds.
|
70
|
+
# @see https://github.com/newrelic/newrelic-telemetry-sdk-specs/blob/master/communication.md#graceful-degradation Graceful Degradation
|
71
|
+
attr_accessor :backoff_max
|
72
|
+
|
73
|
+
# @!attribute max_retries [optional, Integer]
|
74
|
+
# The maximum number of times to retry sending data to New Relic.
|
75
|
+
# Defaults to {DEFAULT_MAX_RETRIES}.
|
76
|
+
attr_accessor :max_retries
|
77
|
+
|
78
|
+
# @!attribute trace_api_host [optional, String]
|
79
|
+
# An alternative New Relic host URL where spans can be sent.
|
80
|
+
# Defaults to {DEFAULT_TRACE_API_HOST}
|
81
|
+
attr_accessor :trace_api_host
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def initialize
|
85
|
+
@api_insert_key = ENV[API_INSERT_KEY]
|
86
|
+
@logger = Logger.logger
|
87
|
+
@audit_logging_enabled = false
|
88
|
+
|
89
|
+
@harvest_interval = DEFAULT_HARVEST_INTERVAL
|
90
|
+
@backoff_factor = DEFAULT_BACKOFF_FACTOR
|
91
|
+
@backoff_max = DEFAULT_BACKOFF_MAX
|
92
|
+
@max_retries = DEFAULT_MAX_RETRIES
|
93
|
+
@trace_api_host = DEFAULT_TRACE_API_HOST
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# This file is distributed under New Relic's license terms.
|
4
|
+
# See https://github.com/newrelic/newrelic-telemetry-sdk-ruby/blob/main/LICENSE for complete details.
|
5
|
+
|
6
|
+
module NewRelic
|
7
|
+
module TelemetrySdk
|
8
|
+
# The {Configurator} provides the mechanism through which the SDK is configured.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# NewRelic::TelemetrySdk.configure do |config|
|
12
|
+
# config.api_insert_key = ENV["API_KEY"]
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# See {Config} for details on what properties can be configured.
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
class Configurator
|
19
|
+
def self.config
|
20
|
+
@config ||= Config.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Removes any configurations that were previously customized, effectively
|
24
|
+
# resetting the {Config} state to defaults
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
def self.reset
|
28
|
+
@config = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Allows direct access to the config state. The primary purpose of this method is
|
32
|
+
# to access config properties throughout the SDK.
|
33
|
+
#
|
34
|
+
# @note Unlike configuring with # {#self.configure}, setting config properties here
|
35
|
+
# may, or may not become immediately active. Use with care.
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def config
|
39
|
+
Configurator.config
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set Telemetry SDK configuration with a block.
|
43
|
+
# See {Config} for options.
|
44
|
+
#
|
45
|
+
# @example Setting the API Key
|
46
|
+
# NewRelic::TelemetrySdk.configure do |config|
|
47
|
+
# config.api_insert_key = ENV["API_KEY"]
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def configure
|
52
|
+
Logger.logger = config.logger
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Delegates any setter methods to the Config object if it responds to such.
|
58
|
+
# All other missing methods are propagated up the chain.
|
59
|
+
def method_missing method, *args, &block
|
60
|
+
if method.to_s =~ /\=$/ && config.respond_to?(method)
|
61
|
+
config.send method, *args, &block
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set Telemetry SDK configuration with a block.
|
69
|
+
# See {Config} for options.
|
70
|
+
#
|
71
|
+
#
|
72
|
+
# @example Setting the API Key
|
73
|
+
# NewRelic::TelemetrySdk.configure do |config|
|
74
|
+
# config.api_insert_key = ENV["API_KEY"]
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def self.configure
|
79
|
+
configurator = Configurator.new
|
80
|
+
yield configurator if block_given?
|
81
|
+
configurator.configure
|
82
|
+
end
|
83
|
+
|
84
|
+
# Allows direct access to the config state. The primary purpose of this method is
|
85
|
+
# to access config properties throughout the SDK.
|
86
|
+
#
|
87
|
+
# @note Unlike configuring with # {#self.configure}, setting config properties here
|
88
|
+
# may, or may not become immediately active. Use with care.
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def self.config
|
92
|
+
Configurator.config
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# This file is distributed under New Relic's license terms.
|
4
|
+
# See https://github.com/newrelic/newrelic-telemetry-sdk-ruby/blob/main/LICENSE for complete details.
|
5
|
+
|
6
|
+
module NewRelic
|
7
|
+
module TelemetrySdk
|
8
|
+
# A {RetriableServerResponseException} is used to signal that the SDK
|
9
|
+
# should attempt to resend the data that received a response error
|
10
|
+
# from the server on the previous attempt.
|
11
|
+
# @private
|
12
|
+
class RetriableServerResponseException < StandardError; end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# This file is distributed under New Relic's license terms.
|
3
|
+
# See https://github.com/newrelic/newrelic-telemetry-sdk-ruby/blob/main/LICENSE for complete details.
|
4
|
+
|
5
|
+
module NewRelic
|
6
|
+
module TelemetrySdk
|
7
|
+
# This class handles sending data to New Relic automatically at configured
|
8
|
+
# intervals.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# harvester = NewRelic::TelemetrySdk::Harvester.new
|
12
|
+
# trace_client = NewRelic::TelemetrySdk::TraceClient.new
|
13
|
+
# buffer = NewRelic::TelemetrySdk::Buffer.new
|
14
|
+
# harvester.register 'external_spans', buffer, trace_client
|
15
|
+
# harvester.start
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
class Harvester
|
19
|
+
include NewRelic::TelemetrySdk::Logger
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@pipelines = {}
|
23
|
+
@shutdown = false
|
24
|
+
@running = false
|
25
|
+
@lock = Mutex.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Register a pipeline (i.e. a buffer from which data can be harvested
|
29
|
+
# via a +flush+ method and a client that can be used to send that data).
|
30
|
+
# @param name [String]
|
31
|
+
# A unique name for the type of data associated with this pipeline.
|
32
|
+
# Examples: 'spans', 'external_spans'
|
33
|
+
# @param buffer [Buffer]
|
34
|
+
# An instance of NewRelic::TelemetrySdk::Buffer in which data can be
|
35
|
+
# stored for harvest.
|
36
|
+
# @param client [Client]
|
37
|
+
# An instance of a NewRelic::TelemetrySdk::Client subclass which will
|
38
|
+
# send harvested data to the correct New Relic backend (e.g. TraceClient
|
39
|
+
# for spans).
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def register name, buffer, client
|
43
|
+
logger.info "Registering pipeline #{name}"
|
44
|
+
@lock.synchronize do
|
45
|
+
@pipelines[name] = {
|
46
|
+
buffer: buffer,
|
47
|
+
client: client
|
48
|
+
}
|
49
|
+
end
|
50
|
+
rescue => e
|
51
|
+
log_error "Encountered error while registering buffer #{name}.", e
|
52
|
+
end
|
53
|
+
|
54
|
+
def [] name
|
55
|
+
@pipelines[name]
|
56
|
+
end
|
57
|
+
|
58
|
+
def interval
|
59
|
+
TelemetrySdk.config.harvest_interval
|
60
|
+
end
|
61
|
+
|
62
|
+
def running?
|
63
|
+
@running
|
64
|
+
end
|
65
|
+
|
66
|
+
# Start scheduled harvests via this harvester.
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
def start
|
70
|
+
logger.info "Harvesting every #{interval} seconds"
|
71
|
+
@running = true
|
72
|
+
@harvest_thread = Thread.new do
|
73
|
+
begin
|
74
|
+
while !@shutdown do
|
75
|
+
sleep interval
|
76
|
+
harvest
|
77
|
+
end
|
78
|
+
harvest
|
79
|
+
@running = false
|
80
|
+
rescue => e
|
81
|
+
log_error "Encountered error in harvester", e
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Stop scheduled harvests via this harvester. Any remaining
|
87
|
+
# buffered data will be sent before the harvest thread is stopped.
|
88
|
+
#
|
89
|
+
# @api public
|
90
|
+
def stop
|
91
|
+
logger.info "Stopping harvester"
|
92
|
+
@shutdown = true
|
93
|
+
@harvest_thread.join if @running
|
94
|
+
rescue => e
|
95
|
+
log_error "Encountered error stopping harvester", e
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def harvest
|
101
|
+
@lock.synchronize do
|
102
|
+
@pipelines.values.each do |pipeline|
|
103
|
+
send_data_via pipeline
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def send_data_via pipeline
|
109
|
+
batch = pipeline[:buffer].flush
|
110
|
+
if !batch.nil? && batch[0].respond_to?(:any?) && batch[0].any?
|
111
|
+
pipeline[:client].report_batch batch
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# This file is distributed under New Relic's license terms.
|
4
|
+
# See https://github.com/newrelic/newrelic-telemetry-sdk-ruby/blob/main/LICENSE for complete details.
|
5
|
+
|
6
|
+
module NewRelic
|
7
|
+
module TelemetrySdk
|
8
|
+
# The Logger Singleton object manages the logger for the SDK and is fully configurable by the user.
|
9
|
+
# Any Ruby class that responds to the common methods of the Ruby standard {::Logger} class can
|
10
|
+
# be configured for the SDk.
|
11
|
+
#
|
12
|
+
# The logger may be configured like this:
|
13
|
+
# @example with configure block
|
14
|
+
# require 'logger'
|
15
|
+
#
|
16
|
+
# logger = ::Logger.new(STDOUT)
|
17
|
+
# logger.level = Logger::WARN
|
18
|
+
#
|
19
|
+
# NewRelic::TelemetrySdk.configure do |config|
|
20
|
+
# config.logger = logger
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example without configure block
|
24
|
+
# require 'logger'
|
25
|
+
#
|
26
|
+
# NewRelic::TelemetrySdk.logger = ::Logger.new("/dev/null")
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
module Logger
|
30
|
+
LOG_LEVELS = %w{debug info warn error fatal}
|
31
|
+
private_constant :LOG_LEVELS
|
32
|
+
|
33
|
+
LOG_LEVELS.each do |level|
|
34
|
+
define_method "log_#{level}_once" do |key, *msgs|
|
35
|
+
log_once level, key, *msgs
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.logger= logger
|
40
|
+
@logger = logger
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.logger
|
44
|
+
@logger ||= ::Logger.new(STDOUT)
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_once(level, key, *msgs)
|
48
|
+
logger_mutex.synchronize do
|
49
|
+
return if already_logged.include?(key)
|
50
|
+
already_logged[key] = true
|
51
|
+
end
|
52
|
+
|
53
|
+
logger.send(level, *msgs)
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear_already_logged
|
57
|
+
logger_mutex.synchronize do
|
58
|
+
@already_logged = {}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def log_error(message, exception = nil)
|
63
|
+
logger.error message
|
64
|
+
logger.error exception if exception
|
65
|
+
end
|
66
|
+
|
67
|
+
def logger
|
68
|
+
Logger.logger
|
69
|
+
end
|
70
|
+
|
71
|
+
def logger= logger
|
72
|
+
Logger.logger = logger
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def logger_mutex
|
78
|
+
@logger_mutex ||= Mutex.new
|
79
|
+
end
|
80
|
+
|
81
|
+
def already_logged
|
82
|
+
@already_logged ||= {}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.logger
|
87
|
+
Logger.logger
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|