newrelic-telemetry_sdk 0.1.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 +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
|