solarwinds_apm 5.0.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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +5 -0
  3. data/.github/ISSUE_TEMPLATE/bug-or-feature-request.md +16 -0
  4. data/.github/workflows/build_and_release_gem.yml +112 -0
  5. data/.github/workflows/build_for_packagecloud.yml +70 -0
  6. data/.github/workflows/docker-images.yml +47 -0
  7. data/.github/workflows/run_cpluplus_tests.yml +73 -0
  8. data/.github/workflows/run_tests.yml +155 -0
  9. data/.github/workflows/scripts/test_install.rb +23 -0
  10. data/.github/workflows/swig/swig-v4.0.2.tar.gz +0 -0
  11. data/.github/workflows/test_on_4_linux.yml +161 -0
  12. data/.gitignore +39 -0
  13. data/.rubocop.yml +29 -0
  14. data/.yardopts +7 -0
  15. data/CHANGELOG.md +769 -0
  16. data/CONFIG.md +31 -0
  17. data/Gemfile +14 -0
  18. data/LICENSE +202 -0
  19. data/README.md +383 -0
  20. data/bin/solarwinds_apm_config +15 -0
  21. data/examples/prepend.rb +13 -0
  22. data/examples/sdk_examples.rb +158 -0
  23. data/ext/oboe_metal/README.md +69 -0
  24. data/ext/oboe_metal/extconf.rb +141 -0
  25. data/ext/oboe_metal/extconf_local.rb +75 -0
  26. data/ext/oboe_metal/lib/.keep +0 -0
  27. data/ext/oboe_metal/lib/liboboe-1.0-alpine-x86_64.so.0.0.0.sha256 +1 -0
  28. data/ext/oboe_metal/lib/liboboe-1.0-x86_64.so.0.0.0.sha256 +1 -0
  29. data/ext/oboe_metal/noop/noop.c +8 -0
  30. data/ext/oboe_metal/src/README.md +6 -0
  31. data/ext/oboe_metal/src/VERSION +2 -0
  32. data/ext/oboe_metal/src/bson/bson.h +220 -0
  33. data/ext/oboe_metal/src/bson/platform_hacks.h +91 -0
  34. data/ext/oboe_metal/src/frames.cc +247 -0
  35. data/ext/oboe_metal/src/frames.h +40 -0
  36. data/ext/oboe_metal/src/init_solarwinds_apm.cc +21 -0
  37. data/ext/oboe_metal/src/logging.cc +95 -0
  38. data/ext/oboe_metal/src/logging.h +35 -0
  39. data/ext/oboe_metal/src/oboe.h +1169 -0
  40. data/ext/oboe_metal/src/oboe_api.cpp +658 -0
  41. data/ext/oboe_metal/src/oboe_api.hpp +433 -0
  42. data/ext/oboe_metal/src/oboe_debug.h +59 -0
  43. data/ext/oboe_metal/src/oboe_swig_wrap.cc +7562 -0
  44. data/ext/oboe_metal/src/profiling.cc +435 -0
  45. data/ext/oboe_metal/src/profiling.h +78 -0
  46. data/ext/oboe_metal/test/CMakeLists.txt +53 -0
  47. data/ext/oboe_metal/test/FindGMock.cmake +43 -0
  48. data/ext/oboe_metal/test/README.md +56 -0
  49. data/ext/oboe_metal/test/frames_test.cc +164 -0
  50. data/ext/oboe_metal/test/profiling_test.cc +93 -0
  51. data/ext/oboe_metal/test/ruby_inc_dir.rb +8 -0
  52. data/ext/oboe_metal/test/ruby_prefix.rb +8 -0
  53. data/ext/oboe_metal/test/ruby_test_helper.rb +67 -0
  54. data/ext/oboe_metal/test/test.h +11 -0
  55. data/ext/oboe_metal/test/test_main.cc +32 -0
  56. data/init.rb +4 -0
  57. data/lib/oboe.rb +7 -0
  58. data/lib/oboe_metal.rb +172 -0
  59. data/lib/rails/generators/solarwinds_apm/install_generator.rb +47 -0
  60. data/lib/rails/generators/solarwinds_apm/templates/solarwinds_apm_initializer.rb +424 -0
  61. data/lib/solarwinds_apm/api/layerinit.rb +41 -0
  62. data/lib/solarwinds_apm/api/logging.rb +356 -0
  63. data/lib/solarwinds_apm/api/memcache.rb +37 -0
  64. data/lib/solarwinds_apm/api/metrics.rb +63 -0
  65. data/lib/solarwinds_apm/api/util.rb +98 -0
  66. data/lib/solarwinds_apm/api.rb +21 -0
  67. data/lib/solarwinds_apm/base.rb +160 -0
  68. data/lib/solarwinds_apm/config.rb +301 -0
  69. data/lib/solarwinds_apm/frameworks/grape.rb +96 -0
  70. data/lib/solarwinds_apm/frameworks/padrino.rb +78 -0
  71. data/lib/solarwinds_apm/frameworks/rails/inst/action_controller.rb +100 -0
  72. data/lib/solarwinds_apm/frameworks/rails/inst/action_controller5.rb +50 -0
  73. data/lib/solarwinds_apm/frameworks/rails/inst/action_controller_api.rb +50 -0
  74. data/lib/solarwinds_apm/frameworks/rails/inst/action_view.rb +88 -0
  75. data/lib/solarwinds_apm/frameworks/rails/inst/active_record.rb +26 -0
  76. data/lib/solarwinds_apm/frameworks/rails/inst/connection_adapters/mysql2.rb +29 -0
  77. data/lib/solarwinds_apm/frameworks/rails/inst/connection_adapters/postgresql.rb +22 -0
  78. data/lib/solarwinds_apm/frameworks/rails/inst/connection_adapters/utils5x.rb +103 -0
  79. data/lib/solarwinds_apm/frameworks/rails/inst/logger_formatters.rb +14 -0
  80. data/lib/solarwinds_apm/frameworks/rails.rb +100 -0
  81. data/lib/solarwinds_apm/frameworks/sinatra.rb +96 -0
  82. data/lib/solarwinds_apm/inst/bunny-client.rb +157 -0
  83. data/lib/solarwinds_apm/inst/bunny-consumer.rb +102 -0
  84. data/lib/solarwinds_apm/inst/curb.rb +288 -0
  85. data/lib/solarwinds_apm/inst/dalli.rb +89 -0
  86. data/lib/solarwinds_apm/inst/delayed_job.rb +100 -0
  87. data/lib/solarwinds_apm/inst/excon.rb +113 -0
  88. data/lib/solarwinds_apm/inst/faraday.rb +96 -0
  89. data/lib/solarwinds_apm/inst/graphql.rb +206 -0
  90. data/lib/solarwinds_apm/inst/grpc_client.rb +147 -0
  91. data/lib/solarwinds_apm/inst/grpc_server.rb +119 -0
  92. data/lib/solarwinds_apm/inst/httpclient.rb +181 -0
  93. data/lib/solarwinds_apm/inst/logger_formatter.rb +46 -0
  94. data/lib/solarwinds_apm/inst/logging_log_event.rb +24 -0
  95. data/lib/solarwinds_apm/inst/lumberjack_formatter.rb +9 -0
  96. data/lib/solarwinds_apm/inst/memcached.rb +86 -0
  97. data/lib/solarwinds_apm/inst/mongo.rb +246 -0
  98. data/lib/solarwinds_apm/inst/mongo2.rb +225 -0
  99. data/lib/solarwinds_apm/inst/moped.rb +466 -0
  100. data/lib/solarwinds_apm/inst/net_http.rb +60 -0
  101. data/lib/solarwinds_apm/inst/rack.rb +217 -0
  102. data/lib/solarwinds_apm/inst/rack_cache.rb +35 -0
  103. data/lib/solarwinds_apm/inst/redis.rb +273 -0
  104. data/lib/solarwinds_apm/inst/resque.rb +129 -0
  105. data/lib/solarwinds_apm/inst/rest-client.rb +43 -0
  106. data/lib/solarwinds_apm/inst/sequel.rb +241 -0
  107. data/lib/solarwinds_apm/inst/sidekiq-client.rb +63 -0
  108. data/lib/solarwinds_apm/inst/sidekiq-worker.rb +64 -0
  109. data/lib/solarwinds_apm/inst/typhoeus.rb +90 -0
  110. data/lib/solarwinds_apm/instrumentation.rb +22 -0
  111. data/lib/solarwinds_apm/loading.rb +65 -0
  112. data/lib/solarwinds_apm/logger.rb +14 -0
  113. data/lib/solarwinds_apm/noop/README.md +9 -0
  114. data/lib/solarwinds_apm/noop/context.rb +26 -0
  115. data/lib/solarwinds_apm/noop/metadata.rb +25 -0
  116. data/lib/solarwinds_apm/noop/profiling.rb +21 -0
  117. data/lib/solarwinds_apm/oboe_init_options.rb +191 -0
  118. data/lib/solarwinds_apm/ruby.rb +35 -0
  119. data/lib/solarwinds_apm/sdk/current_trace_info.rb +123 -0
  120. data/lib/solarwinds_apm/sdk/custom_metrics.rb +94 -0
  121. data/lib/solarwinds_apm/sdk/logging.rb +37 -0
  122. data/lib/solarwinds_apm/sdk/trace_context_headers.rb +69 -0
  123. data/lib/solarwinds_apm/sdk/tracing.rb +432 -0
  124. data/lib/solarwinds_apm/support/profiling.rb +22 -0
  125. data/lib/solarwinds_apm/support/trace_context.rb +53 -0
  126. data/lib/solarwinds_apm/support/trace_state.rb +69 -0
  127. data/lib/solarwinds_apm/support/trace_string.rb +89 -0
  128. data/lib/solarwinds_apm/support/transaction_metrics.rb +67 -0
  129. data/lib/solarwinds_apm/support/transaction_settings.rb +233 -0
  130. data/lib/solarwinds_apm/support/x_trace_options.rb +113 -0
  131. data/lib/solarwinds_apm/support.rb +12 -0
  132. data/lib/solarwinds_apm/support_report.rb +113 -0
  133. data/lib/solarwinds_apm/test.rb +165 -0
  134. data/lib/solarwinds_apm/thread_local.rb +26 -0
  135. data/lib/solarwinds_apm/util.rb +334 -0
  136. data/lib/solarwinds_apm/version.rb +17 -0
  137. data/lib/solarwinds_apm.rb +72 -0
  138. data/log/.keep +0 -0
  139. data/log/postgresql/.keep +0 -0
  140. data/solarwinds_apm.gemspec +52 -0
  141. data/yardoc_frontpage.md +24 -0
  142. metadata +228 -0
@@ -0,0 +1,100 @@
1
+ # Copyright (c) 2016 SolarWinds, LLC.
2
+ # All rights reserved.
3
+
4
+ if defined?(Delayed)
5
+ module SolarWindsAPM
6
+ module Inst
7
+ module DelayedJob
8
+ ##
9
+ # ForkHandler
10
+ #
11
+ # Since delayed job doesn't offer a hook into `after_fork`, we alias the method
12
+ # here to do our magic after a fork happens.
13
+ #
14
+ module ForkHandler
15
+ def self.extended(klass)
16
+ SolarWindsAPM::Util.class_method_alias(klass, :after_fork, ::Delayed::Worker)
17
+ end
18
+
19
+ def after_fork_with_sw_apm
20
+ SolarWindsAPM.logger.info '[solarwinds_apm/delayed_job] Detected fork. Restarting SolarWindsAPM reporter.' if SolarWindsAPM::Config[:verbose]
21
+ SolarWindsAPM::Reporter.restart unless ENV.key?('SW_APM_GEM_TEST')
22
+
23
+ after_fork_without_sw_apm
24
+ end
25
+ end
26
+
27
+ ##
28
+ # SolarWindsAPM::Inst::DelayedJob::Plugin
29
+ #
30
+ # The SolarWindsAPM DelayedJob plugin. Here we wrap `enqueue` and
31
+ # `perform` to capture the timing of the bits we're interested in.
32
+ #
33
+ # Traces from the client are not continued in the consumer for a number
34
+ # of reasons:
35
+ # - no context propagation for delayed_job in OTEL
36
+ # - there is no reliable way to for the job to carry trace information. It
37
+ # is an instance of a shared class they share,
38
+ # often: Delayed::Backend::ActiveRecord::Job, but could be something else
39
+ # - It can also be too asynchronous for tracing to make sense. The worker can be
40
+ # delayed by seconds/minutes/hours and the trace processing completed already
41
+ #
42
+ class Plugin < Delayed::Plugin
43
+ callbacks do |lifecycle|
44
+
45
+ # enqueue
46
+ if SolarWindsAPM::Config[:delayed_jobclient][:enabled]
47
+ lifecycle.around(:enqueue) do |job, &block|
48
+ begin
49
+ report_kvs = {}
50
+ report_kvs[:Spec] = :pushq
51
+ report_kvs[:Flavor] = :DelayedJob
52
+ report_kvs[:JobName] = job.name
53
+ report_kvs[:MsgID] = job.id
54
+ report_kvs[:Queue] = job.queue if job.queue
55
+ report_kvs[:Backtrace] = SolarWindsAPM::API.backtrace if SolarWindsAPM::Config[:delayed_jobclient][:collect_backtraces]
56
+
57
+ SolarWindsAPM::SDK.trace(:'delayed_job-client', kvs: report_kvs) do
58
+ block.call(job)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # invoke_job
65
+ if SolarWindsAPM::Config[:delayed_jobworker][:enabled]
66
+ lifecycle.around(:perform) do |worker, job, &block|
67
+ begin
68
+ report_kvs = {}
69
+ report_kvs[:Spec] = :job
70
+ report_kvs[:Flavor] = :DelayedJob
71
+ report_kvs[:JobName] = job.name
72
+ report_kvs[:MsgID] = job.id
73
+ report_kvs[:Queue] = job.queue if job.queue
74
+ report_kvs[:Backtrace] = SolarWindsAPM::API.backtrace if SolarWindsAPM::Config[:delayed_jobworker][:collect_backtraces]
75
+
76
+ # DelayedJob Specific KVs
77
+ report_kvs[:priority] = job.priority
78
+ report_kvs[:attempts] = job.attempts
79
+ report_kvs[:WorkerName] = job.locked_by
80
+ rescue => e
81
+ SolarWindsAPM.logger.warn "[solarwinds_apm/warning] inst/delayed_job.rb: #{e.message}"
82
+ end
83
+
84
+ SolarWindsAPM::SDK.start_trace(:'delayed_job-worker', kvs: report_kvs) do
85
+ result = block.call(worker, job)
86
+ SolarWindsAPM::API.log_exception(:'delayed_job-worker', job.error) if job.error
87
+ result
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ SolarWindsAPM.logger.info '[solarwinds_apm/loading] Instrumenting delayed_job' if SolarWindsAPM::Config[:verbose]
98
+ SolarWindsAPM::Util.send_extend(::Delayed::Worker, SolarWindsAPM::Inst::DelayedJob::ForkHandler)
99
+ Delayed::Worker.plugins << SolarWindsAPM::Inst::DelayedJob::Plugin
100
+ end
@@ -0,0 +1,113 @@
1
+ # Copyright (c) 2016 SolarWinds, LLC.
2
+ # All rights reserved.
3
+
4
+ module SolarWindsAPM
5
+ module Inst
6
+ module ExconConnection
7
+ include SolarWindsAPM::SDK::TraceContextHeaders
8
+
9
+ private
10
+
11
+ def sw_apm_collect(params)
12
+ kvs = {}
13
+ kvs[:Spec] = 'rsc'
14
+ kvs[:IsService] = 1
15
+
16
+ # Conditionally log query args
17
+ if SolarWindsAPM::Config[:excon][:log_args] && @data[:query]
18
+ if @data[:query].is_a?(Hash)
19
+ service_arg = "#{@data[:path]}?#{URI.encode_www_form(@data[:query])}"
20
+ else
21
+ service_arg = "#{@data[:path]}?#{@data[:query]}"
22
+ end
23
+ else
24
+ service_arg = @data[:path]
25
+ end
26
+ kvs[:RemoteURL] = "#{@data[:scheme]}://#{@data[:host]}:#{@data[:port]}#{service_arg}"
27
+
28
+ # In the case of HTTP pipelining, params could be an array of
29
+ # request hashes.
30
+ if params.is_a?(Array)
31
+ methods = []
32
+ params.each do |p|
33
+ methods << SolarWindsAPM::Util.upcase(p[:method])
34
+ end
35
+ kvs[:HTTPMethods] = methods.join(',')[0..1024]
36
+ kvs[:Pipeline] = true
37
+ else
38
+ kvs[:HTTPMethod] = SolarWindsAPM::Util.upcase(params[:method])
39
+ end
40
+ kvs
41
+ rescue => e
42
+ SolarWindsAPM.logger.debug "[solarwinds_apm/debug] Error capturing excon KVs: #{e.message}"
43
+ SolarWindsAPM.logger.debug e.backtrace.join('\n') if SolarWindsAPM::Config[:verbose]
44
+ ensure
45
+ return kvs
46
+ end
47
+
48
+ public
49
+
50
+ def requests(pipeline_params)
51
+ responses = nil
52
+ kvs = sw_apm_collect(pipeline_params)
53
+ SolarWindsAPM::SDK.trace(:excon, kvs: kvs) do
54
+ kvs[:Backtrace] = SolarWindsAPM::API.backtrace if SolarWindsAPM::Config[:excon][:collect_backtraces]
55
+ responses = super(pipeline_params)
56
+ kvs[:HTTPStatuses] = responses.map { |r| r.status }.join(',')
57
+ end
58
+ responses
59
+ end
60
+
61
+ def request(params = {}, &block)
62
+ # If we're not tracing, just do a fast return.
63
+ # If making HTTP pipeline requests (ordered batched)
64
+ # then just return as we're tracing from parent
65
+ # <tt>requests</tt>
66
+ if !SolarWindsAPM.tracing? || params[:pipeline]
67
+ add_tracecontext_headers(@data[:headers])
68
+ return super(params, &block)
69
+ end
70
+
71
+ begin
72
+ response_context = nil
73
+
74
+ kvs = sw_apm_collect(params)
75
+
76
+ SolarWindsAPM::API.log_entry(:excon, kvs)
77
+ kvs.clear
78
+
79
+ req_context = SolarWindsAPM::Context.toString
80
+
81
+ # The core excon call
82
+ add_tracecontext_headers(@data[:headers])
83
+ response = super(params, &block)
84
+
85
+ # excon only passes back a hash (datum) for HTTP pipelining...
86
+ # In that case, we should never arrive here but for the OCD, double check
87
+ # the datatype before trying to extract pertinent info
88
+ if response.is_a?(Excon::Response)
89
+ kvs[:HTTPStatus] = response.status
90
+
91
+ # If we get a redirect, report the location header
92
+ if ((300..308).to_a.include? response.status.to_i) && response.headers.key?('Location')
93
+ kvs[:Location] = response.headers['Location']
94
+ end
95
+ end
96
+
97
+ response
98
+ rescue => e
99
+ SolarWindsAPM::API.log_exception(:excon, e)
100
+ raise e
101
+ ensure
102
+ kvs[:Backtrace] = SolarWindsAPM::API.backtrace if SolarWindsAPM::Config[:excon][:collect_backtraces]
103
+ SolarWindsAPM::API.log_exit(:excon, kvs) unless params[:pipeline]
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ if SolarWindsAPM::Config[:excon][:enabled] && defined?(Excon)
111
+ SolarWindsAPM.logger.info '[solarwinds_apm/loading] Instrumenting excon' if SolarWindsAPM::Config[:verbose]
112
+ Excon::Connection.prepend(SolarWindsAPM::Inst::ExconConnection)
113
+ end
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2016 SolarWinds, LLC.
2
+ # All rights reserved.
3
+
4
+ #####################################################
5
+ # FYI:
6
+ # Faraday only adds tracing when it is
7
+ # not using an adapter that is instrumented
8
+ #
9
+ # otherwise we would get two spans for the same call
10
+ #####################################################
11
+
12
+ module SolarWindsAPM
13
+ module Inst
14
+ module FaradayConnection
15
+ include SolarWindsAPM::SDK::TraceContextHeaders
16
+
17
+ def run_request(method, url, body, headers, &block)
18
+ remote_call = remote_call?
19
+ unless SolarWindsAPM.tracing?
20
+ if remote_call
21
+ add_tracecontext_headers(@headers)
22
+ end
23
+ return super(method, url, body, headers, &block)
24
+ end
25
+
26
+ begin
27
+ SolarWindsAPM::API.log_entry(:faraday)
28
+ if remote_call # nothing else is instrumented that could add the w3c context
29
+ add_tracecontext_headers(@headers)
30
+ end
31
+
32
+ result = super(method, url, body, headers, &block)
33
+
34
+ kvs = {}
35
+
36
+ # this seems the safer condition than trying to identify the
37
+ # faraday version when adapter started to work without arg
38
+ # and handlers don't include the adapter anymore
39
+ if @builder.method(:adapter).parameters.find { |ele| ele[0] == :req }
40
+ kvs[:Middleware] = @builder.handlers
41
+ else
42
+ kvs[:Middleware] = [@builder.adapter] + @builder.handlers
43
+ end
44
+ kvs[:Backtrace] = SolarWindsAPM::API.backtrace if SolarWindsAPM::Config[:faraday][:collect_backtraces]
45
+
46
+ # Only send service KVs if we're not using an adapter
47
+ # Otherwise, the adapter instrumentation will send the service KVs
48
+ if remote_call
49
+ kvs.merge!(rsc_kvs(url, method, result))
50
+ end
51
+
52
+ result
53
+ rescue => e
54
+ SolarWindsAPM::API.log_exception(:faraday, e)
55
+ raise e
56
+ ensure
57
+ SolarWindsAPM::API.log_exit(:faraday, kvs)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # This is only considered a remote service call if the middleware/adapter is not instrumented
64
+ def remote_call?
65
+ if @builder.method(:adapter).parameters.find { |ele| ele[0] == :req }
66
+ (@builder.handlers.map(&:name) & SW_APM_INSTR_ADAPTERS).count == 0
67
+ else
68
+ ((@builder.handlers.map(&:name) << @builder.adapter.name) & SW_APM_INSTR_ADAPTERS).count == 0
69
+ end
70
+ end
71
+
72
+ def rsc_kvs(_url, method, result)
73
+ kvs = { :Spec => 'rsc',
74
+ :IsService => 1,
75
+ :HTTPMethod => method.upcase,
76
+ :HTTPStatus => result.status, }
77
+ kvs[:RemoteURL] = result.env.to_hash[:url].to_s
78
+ kvs[:RemoteURL].split('?').first unless SolarWindsAPM::Config[:faraday][:log_args]
79
+
80
+ kvs
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ if defined?(Faraday) && SolarWindsAPM::Config[:faraday][:enabled]
87
+ SolarWindsAPM.logger.info '[solarwinds_apm/loading] Instrumenting faraday' if SolarWindsAPM::Config[:verbose]
88
+ Faraday::Connection.prepend(SolarWindsAPM::Inst::FaradayConnection)
89
+
90
+ SW_APM_INSTR_ADAPTERS = [] # ["Faraday::Adapter::NetHttp", "Faraday::Adapter::Excon", "Faraday::Adapter::Typhoeus"]
91
+
92
+ SW_APM_INSTR_ADAPTERS << "Faraday::Adapter::Typhoeus" if defined? Faraday::Adapter::Typhoeus
93
+ SW_APM_INSTR_ADAPTERS << "Faraday::Adapter::NetHttp" if defined? Faraday::Adapter::NetHttp
94
+ SW_APM_INSTR_ADAPTERS << "Faraday::Adapter::Excon" if defined? Faraday::Adapter::Excon
95
+ SW_APM_INSTR_ADAPTERS << "Faraday::Adapter::HTTPClient" if defined? Faraday::Adapter::HTTPClient
96
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Copyright (c) 2019 SolarWinds, LLC.
5
+ # All rights reserved.
6
+ #++
7
+ #
8
+
9
+ ##
10
+ # Tracing for the graphql gem
11
+ #
12
+ # This instrumentation is autoloaded when a class inherits GraphQL::Schema
13
+ # no need to call `tracer` or `use`
14
+
15
+
16
+ if defined?(GraphQL::Tracing) && !(SolarWindsAPM::Config[:graphql][:enabled] == false)
17
+ module GraphQL
18
+ module Tracing
19
+
20
+ class SolarWindsAPMTracing < GraphQL::Tracing::PlatformTracing
21
+ # These GraphQL events will show up as 'graphql.prep' spans
22
+ PREP_KEYS = ['lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex'].freeze
23
+ EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
24
+
25
+ self.platform_keys = {
26
+ 'lex' => 'lex',
27
+ 'parse' => 'parse',
28
+ 'validate' => 'validate',
29
+ 'analyze_query' => 'analyze_query',
30
+ 'analyze_multiplex' => 'analyze_multiplex',
31
+ 'execute_multiplex' => 'execute_multiplex',
32
+ 'execute_query' => 'execute_query',
33
+ 'execute_query_lazy' => 'execute_query_lazy'
34
+ }
35
+
36
+ def platform_trace(platform_key, _key, data)
37
+ return yield if gql_config[:enabled] == false
38
+
39
+ layer = span_name(platform_key)
40
+ kvs = metadata(data, layer)
41
+ kvs[:Key] = platform_key if (PREP_KEYS + EXEC_KEYS).include?(platform_key)
42
+
43
+ transaction_name(kvs[:InboundQuery]) if kvs[:InboundQuery] && layer == 'graphql.execute'
44
+
45
+ ::SolarWindsAPM::SDK.trace(layer, kvs: kvs) do
46
+ kvs.clear # we don't have to send them twice
47
+ yield
48
+ end
49
+ end
50
+
51
+ def platform_field_key(type, field)
52
+ "graphql.#{type.graphql_name}.#{field.name}"
53
+ end
54
+
55
+ def platform_authorized_key(type)
56
+ "graphql.#{type.graphql_name}.authorized"
57
+ end
58
+
59
+ def platform_resolve_type_key(type)
60
+ "graphql.#{type.graphql_name}.resolve_type"
61
+ end
62
+
63
+ private
64
+
65
+ def gql_config
66
+ ::SolarWindsAPM::Config[:graphql] ||= {}
67
+ end
68
+
69
+ def transaction_name(query)
70
+ return if gql_config[:transaction_name] == false ||
71
+ ::SolarWindsAPM::SDK.get_transaction_name
72
+
73
+ split_query = query.strip.split(/\W+/, 3)
74
+ split_query[0] = 'query' if split_query[0].empty?
75
+ name = "graphql.#{split_query[0..1].join('.')}"
76
+
77
+ ::SolarWindsAPM::SDK.set_transaction_name(name)
78
+ end
79
+
80
+ def multiplex_transaction_name(names)
81
+ return if gql_config[:transaction_name] == false ||
82
+ ::SolarWindsAPM::SDK.get_transaction_name
83
+
84
+ name = "graphql.multiplex.#{names.join('.')}"
85
+ name = "#{name[0..251]}..." if name.length > 254
86
+
87
+ ::SolarWindsAPM::SDK.set_transaction_name(name)
88
+ end
89
+
90
+ def span_name(key)
91
+ return 'graphql.prep' if PREP_KEYS.include?(key)
92
+ return 'graphql.execute' if EXEC_KEYS.include?(key)
93
+
94
+ key[/^graphql\./] ? key : "graphql.#{key}"
95
+ end
96
+
97
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
98
+ def metadata(data, layer)
99
+ kvs = data.keys.map do |key|
100
+ case key
101
+ when :context
102
+ graphql_context(data[:context], layer)
103
+ when :query
104
+ graphql_query(data[:query])
105
+ when :query_string
106
+ graphql_query_string(data[:query_string])
107
+ when :multiplex
108
+ graphql_multiplex(data[:multiplex])
109
+ else
110
+ [key, data[key]] unless key == :path # we get the path from context
111
+ end
112
+ end
113
+
114
+ kvs.compact.flatten.each_slice(2).to_h.merge(Spec: 'graphql')
115
+ end
116
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
117
+
118
+ def graphql_context(context, layer)
119
+ context.errors && context.errors.each do |err|
120
+ SolarWindsAPM::API.log_exception(layer, err)
121
+ end
122
+
123
+ [[:Path, context.path.join('.')]]
124
+ end
125
+
126
+ def graphql_query(query)
127
+ return [] unless query
128
+
129
+ query_string = query.query_string
130
+ query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
131
+ query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
132
+
133
+ [[:InboundQuery, query_string],
134
+ [:Operation, query.selected_operation_name]]
135
+ end
136
+
137
+ def graphql_query_string(query_string)
138
+ query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
139
+ query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
140
+
141
+ [:InboundQuery, query_string]
142
+ end
143
+
144
+ def graphql_multiplex(data)
145
+ names = data.queries.map(&:operations).map(&:keys).flatten.compact
146
+ multiplex_transaction_name(names) if names.size > 1
147
+
148
+ [:Operations, names.join(', ')]
149
+ end
150
+
151
+ def sanitize(query)
152
+ return unless query
153
+
154
+ # remove arguments
155
+ query.gsub(/"[^"]*"/, '"?"') # strings
156
+ .gsub(/-?[0-9]*\.?[0-9]+e?[0-9]*/, '?') # ints + floats
157
+ .gsub(/\[[^\]]*\]/, '[?]') # arrays
158
+ end
159
+
160
+ def remove_comments(query)
161
+ return unless query
162
+
163
+ query.gsub(/#[^\n\r]*/, '')
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ module SolarWindsAPM
170
+ module GraphQLSchemaPrepend
171
+
172
+ # Graphql doesn't check if a plugin is added twice
173
+ # we would get double traces
174
+ def use(plugin, **options)
175
+ super unless self.plugins.find { |pl| pl[0].to_s == plugin.to_s }
176
+
177
+ self.plugins
178
+ end
179
+
180
+ def inherited(subclass)
181
+ subclass.use(GraphQL::Tracing::SolarWindsAPMTracing)
182
+ super
183
+ end
184
+ end
185
+
186
+ module GraphQLErrorPrepend
187
+ def initialize(*args)
188
+ super
189
+ bt = SolarWindsAPM::API.backtrace(1)
190
+ set_backtrace(bt) unless self.backtrace
191
+ end
192
+ end
193
+ end
194
+
195
+
196
+ if Gem.loaded_specs['graphql'] && Gem.loaded_specs['graphql'].version >= Gem::Version.new('1.8.0')
197
+ SolarWindsAPM.logger.info '[solarwinds_apm/loading] Instrumenting GraphQL' if SolarWindsAPM::Config[:verbose]
198
+ if defined?(GraphQL::Schema)
199
+ GraphQL::Schema.singleton_class.prepend(SolarWindsAPM::GraphQLSchemaPrepend)
200
+ end
201
+
202
+ if defined?(GraphQL::Error)
203
+ GraphQL::Error.prepend(SolarWindsAPM::GraphQLErrorPrepend)
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,147 @@
1
+ # Copyright (c) 2018 SolarWinds, LLC.
2
+ # All rights reserved.
3
+
4
+ module SolarWindsAPM
5
+ module GRPC
6
+
7
+ module ActiveCall
8
+ include SolarWindsAPM::SDK::TraceContextHeaders
9
+
10
+ if defined? ::GRPC
11
+ StatusCodes = {}
12
+ ::GRPC::Core::StatusCodes.constants.each { |code| StatusCodes[::GRPC::Core::StatusCodes.const_get(code)] = code }
13
+ end
14
+
15
+ def self.included(klass)
16
+ SolarWindsAPM::Util.method_alias(klass, :request_response, ::GRPC::ActiveCall)
17
+ SolarWindsAPM::Util.method_alias(klass, :client_streamer, ::GRPC::ActiveCall)
18
+ SolarWindsAPM::Util.method_alias(klass, :server_streamer, ::GRPC::ActiveCall)
19
+ SolarWindsAPM::Util.method_alias(klass, :bidi_streamer, ::GRPC::ActiveCall)
20
+ end
21
+
22
+ def grpc_tags(method_type, method)
23
+ tags = { 'Spec' => 'rsc',
24
+ 'RemoteURL' => "grpc://#{peer}#{method}",
25
+ 'GRPCMethodType' => method_type,
26
+ 'IsService' => 'True'
27
+ }
28
+ tags
29
+ end
30
+
31
+ def request_response_with_sw_apm(req, metadata: {})
32
+ unary_response(req, type: 'UNARY', metadata: metadata, without: :request_response_without_sw_apm)
33
+ end
34
+
35
+ def client_streamer_with_sw_apm(req, metadata: {})
36
+ unary_response(req, type: 'CLIENT_STREAMING', metadata: metadata, without: :client_streamer_without_sw_apm)
37
+ end
38
+
39
+ def server_streamer_with_sw_apm(req, metadata: {}, &blk)
40
+ @tags = grpc_tags('SERVER_STREAMING', metadata['method'] || metadata_to_send['method'])
41
+ SolarWindsAPM::API.log_entry('grpc-client', @tags)
42
+ add_tracecontext_headers(metadata)
43
+
44
+ patch_receive_and_check_status # need to patch this so that log_exit can be called after the enum is consumed
45
+
46
+ response = server_streamer_without_sw_apm(req, metadata: metadata)
47
+ block_given? ? response.each { |r| yield r } : response
48
+ rescue => e
49
+ # this check is needed because the exception may have been logged in patch_receive_and_check_status
50
+ unless e.instance_variable_get(:@exn_logged)
51
+ SolarWindsAPM::API.log_exception('grpc-client', e)
52
+ SolarWindsAPM::API.log_exit('grpc-client', exit_tags(@tags))
53
+ end
54
+ raise e
55
+ end
56
+
57
+ def bidi_streamer_with_sw_apm(req, metadata: {}, &blk)
58
+ @tags = grpc_tags('BIDI_STREAMING', metadata['method'] || metadata_to_send['method'])
59
+ SolarWindsAPM::API.log_entry('grpc-client', @tags)
60
+ add_tracecontext_headers(metadata)
61
+
62
+ patch_set_input_stream_done
63
+
64
+ response = bidi_streamer_without_sw_apm(req, metadata: metadata)
65
+ block_given? ? response.each { |r| yield r } : response
66
+ rescue => e
67
+ unless e.instance_variable_get(:@exn_logged)
68
+ SolarWindsAPM::API.log_exception('grpc-client', e)
69
+ SolarWindsAPM::API.log_exit('grpc-client', exit_tags(@tags))
70
+ end
71
+ raise e
72
+ end
73
+
74
+ private
75
+
76
+ def unary_response(req, type:, metadata:, without:)
77
+ tags = grpc_tags(type, metadata['method'] || metadata_to_send['method'])
78
+ SolarWindsAPM::SDK.trace('grpc-client', kvs: tags) do
79
+ add_tracecontext_headers(metadata)
80
+ begin
81
+ send(without, req, metadata: metadata)
82
+ ensure
83
+ exit_tags(tags)
84
+ end
85
+ end
86
+ end
87
+
88
+ def patch_receive_and_check_status
89
+ def self.receive_and_check_status # need to patch this so that log_exit can be called after the enum is consumed
90
+ super
91
+ rescue => e
92
+ SolarWindsAPM::API.log_exception('grpc-client', e)
93
+ raise e
94
+ ensure
95
+ SolarWindsAPM::API.log_exit('grpc-client', exit_tags(@tags))
96
+ end
97
+ end
98
+
99
+ def patch_set_input_stream_done
100
+ # need to patch this instance method so that log_exit can be called after the enum is consumed
101
+ def self.set_input_stream_done
102
+ return if status.nil?
103
+ if status.code > 0
104
+ SolarWindsAPM::API.log_exception('grpc-client', $!)
105
+ end
106
+ SolarWindsAPM::API.log_exit('grpc-client', exit_tags(@tags))
107
+ super
108
+ end
109
+ end
110
+
111
+ def exit_tags(tags)
112
+ # we need to translate the status.code, it is not the status.details we want, they are not matching 1:1
113
+ tags['GRPCStatus'] ||= @call.status ? StatusCodes[@call.status.code].to_s : 'UNKNOWN'
114
+ tags['Backtrace'] = SolarWindsAPM::API.backtrace if SolarWindsAPM::Config[:grpc_client][:collect_backtraces]
115
+ tags
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+
122
+ if defined?(GRPC) && SolarWindsAPM::Config[:grpc_client][:enabled]
123
+ SolarWindsAPM.logger.info '[solarwinds_apm/loading] Instrumenting GRPC' if SolarWindsAPM::Config[:verbose]
124
+
125
+ # Client side is instrumented in ActiveCall and ClientStub
126
+ SolarWindsAPM::Util.send_include(GRPC::ActiveCall, SolarWindsAPM::GRPC::ActiveCall)
127
+
128
+ GRPC_ClientStub_ops = [:request_response, :client_streamer, :server_streamer, :bidi_streamer]
129
+ module GRPC
130
+ class ClientStub
131
+ GRPC_ClientStub_ops.reject { |m| !method_defined?(m) }.each do |m|
132
+ define_method("#{m}_with_sw_apm") do |method, req, marshal, unmarshal, deadline: nil,
133
+ return_op: false, parent: nil,
134
+ credentials: nil, metadata: {}, &blk|
135
+
136
+ metadata['method'] = method
137
+ return send("#{m}_without_sw_apm", method, req, marshal, unmarshal, deadline: deadline,
138
+ return_op: return_op, parent: parent,
139
+ credentials: credentials, metadata: metadata, &blk)
140
+ end
141
+
142
+ SolarWindsAPM::Util.method_alias(GRPC::ClientStub, m)
143
+ end
144
+
145
+ end
146
+ end
147
+ end