instana 2.0.0 → 2.2.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/lib/instana/instrumentation/instrumented_request.rb +2 -0
  3. data/lib/instana/instrumentation/net-http.rb +2 -1
  4. data/lib/instana/instrumentation/rack.rb +86 -66
  5. data/lib/instana/setup.rb +1 -0
  6. data/lib/instana/span_filtering/condition.rb +134 -0
  7. data/lib/instana/span_filtering/configuration.rb +262 -0
  8. data/lib/instana/span_filtering/filter_rule.rb +31 -0
  9. data/lib/instana/span_filtering.rb +62 -0
  10. data/lib/instana/trace/span.rb +5 -3
  11. data/lib/instana/trace/span_context.rb +10 -2
  12. data/lib/instana/trace/tracer.rb +0 -21
  13. data/lib/instana/util.rb +13 -0
  14. data/lib/instana/version.rb +1 -1
  15. metadata +7 -258
  16. data/.circleci/config.yml +0 -361
  17. data/.codeclimate.yml +0 -23
  18. data/.editorconfig +0 -10
  19. data/.fasterer.yml +0 -23
  20. data/.github/ISSUE_TEMPLATE/bug.yml +0 -39
  21. data/.github/ISSUE_TEMPLATE/config.yml +0 -8
  22. data/.github/workflows/pr_commits_signed_off.yml +0 -16
  23. data/.github/workflows/release-notification-on-slack.yml +0 -34
  24. data/.gitignore +0 -19
  25. data/.rubocop.yml +0 -34
  26. data/.rubocop_todo.yml +0 -1140
  27. data/.tekton/.currency/docs/report.md +0 -20
  28. data/.tekton/.currency/resources/requirements.txt +0 -4
  29. data/.tekton/.currency/resources/table.json +0 -100
  30. data/.tekton/.currency/scripts/generate_report.py +0 -308
  31. data/.tekton/README.md +0 -278
  32. data/.tekton/github-interceptor-secret.yaml +0 -8
  33. data/.tekton/github-pr-eventlistener.yaml +0 -104
  34. data/.tekton/github-pr-pipeline.yaml.part +0 -38
  35. data/.tekton/github-set-status-task.yaml +0 -43
  36. data/.tekton/github-webhook-ingress.yaml +0 -20
  37. data/.tekton/pipeline.yaml +0 -484
  38. data/.tekton/pipelinerun.yaml +0 -21
  39. data/.tekton/prepuller-restart-service-account.yaml +0 -31
  40. data/.tekton/ruby-tracer-prepuller-cronjob.yaml +0 -20
  41. data/.tekton/ruby-tracer-prepuller.yaml +0 -88
  42. data/.tekton/run_unittests.sh +0 -87
  43. data/.tekton/scheduled-eventlistener.yaml +0 -108
  44. data/.tekton/task.yaml +0 -453
  45. data/.tekton/tekton-triggers-eventlistener-serviceaccount.yaml +0 -29
  46. data/Appraisals +0 -124
  47. data/CONTRIBUTING.md +0 -86
  48. data/Gemfile +0 -22
  49. data/LICENSE +0 -22
  50. data/MAINTAINERS.md +0 -3
  51. data/Rakefile +0 -49
  52. data/bin/announce_release_on_slack.py +0 -103
  53. data/docker-compose.yml +0 -20
  54. data/download.sh +0 -85
  55. data/examples/otel.rb +0 -98
  56. data/examples/tracing.rb +0 -85
  57. data/extras/license_header.rb +0 -44
  58. data/gemfiles/.bundle/config +0 -2
  59. data/gemfiles/aws_30.gemfile +0 -21
  60. data/gemfiles/aws_60.gemfile +0 -16
  61. data/gemfiles/cuba_30.gemfile +0 -16
  62. data/gemfiles/cuba_40.gemfile +0 -13
  63. data/gemfiles/dalli_20.gemfile +0 -15
  64. data/gemfiles/dalli_30.gemfile +0 -12
  65. data/gemfiles/dalli_32.gemfile +0 -12
  66. data/gemfiles/excon_0100.gemfile +0 -14
  67. data/gemfiles/excon_021.gemfile +0 -17
  68. data/gemfiles/excon_079.gemfile +0 -17
  69. data/gemfiles/excon_100.gemfile +0 -14
  70. data/gemfiles/graphql_10.gemfile +0 -16
  71. data/gemfiles/graphql_20.gemfile +0 -15
  72. data/gemfiles/grpc_10.gemfile +0 -15
  73. data/gemfiles/mongo_216.gemfile +0 -15
  74. data/gemfiles/mongo_219.gemfile +0 -12
  75. data/gemfiles/net_http_01.gemfile +0 -17
  76. data/gemfiles/rack_16.gemfile +0 -15
  77. data/gemfiles/rack_20.gemfile +0 -15
  78. data/gemfiles/rack_30.gemfile +0 -13
  79. data/gemfiles/rails_42.gemfile +0 -18
  80. data/gemfiles/rails_50.gemfile +0 -19
  81. data/gemfiles/rails_52.gemfile +0 -19
  82. data/gemfiles/rails_60.gemfile +0 -19
  83. data/gemfiles/rails_61.gemfile +0 -21
  84. data/gemfiles/rails_70.gemfile +0 -18
  85. data/gemfiles/rails_71.gemfile +0 -17
  86. data/gemfiles/rails_80.gemfile +0 -17
  87. data/gemfiles/redis_40.gemfile +0 -15
  88. data/gemfiles/redis_50.gemfile +0 -13
  89. data/gemfiles/redis_51.gemfile +0 -13
  90. data/gemfiles/resque_122.gemfile +0 -16
  91. data/gemfiles/resque_1274_3scale.gemfile +0 -17
  92. data/gemfiles/resque_20.gemfile +0 -16
  93. data/gemfiles/rest_client_16.gemfile +0 -17
  94. data/gemfiles/rest_client_20.gemfile +0 -17
  95. data/gemfiles/roda_20.gemfile +0 -16
  96. data/gemfiles/roda_30.gemfile +0 -16
  97. data/gemfiles/rubocop_162.gemfile +0 -6
  98. data/gemfiles/sequel_56.gemfile +0 -16
  99. data/gemfiles/sequel_57.gemfile +0 -16
  100. data/gemfiles/sequel_58.gemfile +0 -16
  101. data/gemfiles/shoryuken_50.gemfile +0 -16
  102. data/gemfiles/shoryuken_60.gemfile +0 -13
  103. data/gemfiles/sidekiq_42.gemfile +0 -15
  104. data/gemfiles/sidekiq_50.gemfile +0 -15
  105. data/gemfiles/sidekiq_60.gemfile +0 -12
  106. data/gemfiles/sidekiq_65.gemfile +0 -12
  107. data/gemfiles/sidekiq_70.gemfile +0 -12
  108. data/gemfiles/sinatra_14.gemfile +0 -15
  109. data/gemfiles/sinatra_22.gemfile +0 -12
  110. data/gemfiles/sinatra_30.gemfile +0 -12
  111. data/gemfiles/sinatra_40.gemfile +0 -12
  112. data/instana.gemspec +0 -53
  113. data/log/.keep +0 -0
  114. data/sonar-project.properties +0 -9
  115. data/test/activator_test.rb +0 -50
  116. data/test/backend/agent_test.rb +0 -80
  117. data/test/backend/gc_snapshot_test.rb +0 -11
  118. data/test/backend/host_agent_activation_observer_test.rb +0 -73
  119. data/test/backend/host_agent_lookup_test.rb +0 -78
  120. data/test/backend/host_agent_reporting_observer_test.rb +0 -276
  121. data/test/backend/host_agent_test.rb +0 -89
  122. data/test/backend/process_info_test.rb +0 -83
  123. data/test/backend/request_client_test.rb +0 -39
  124. data/test/backend/serverless_agent_test.rb +0 -83
  125. data/test/benchmarks/bench_id_generation.rb +0 -15
  126. data/test/benchmarks/bench_opentracing.rb +0 -16
  127. data/test/config_test.rb +0 -34
  128. data/test/frameworks/cuba_test.rb +0 -61
  129. data/test/frameworks/roda_test.rb +0 -60
  130. data/test/frameworks/sinatra_test.rb +0 -71
  131. data/test/instana_test.rb +0 -37
  132. data/test/instrumentation/aws_test.rb +0 -241
  133. data/test/instrumentation/dalli_test.rb +0 -325
  134. data/test/instrumentation/excon_test.rb +0 -204
  135. data/test/instrumentation/graphql_test.rb +0 -289
  136. data/test/instrumentation/grpc_test.rb +0 -420
  137. data/test/instrumentation/mongo_test.rb +0 -68
  138. data/test/instrumentation/net_http_test.rb +0 -220
  139. data/test/instrumentation/rack_instrumented_request_test.rb +0 -211
  140. data/test/instrumentation/rack_test.rb +0 -415
  141. data/test/instrumentation/rails_action_cable_test.rb +0 -135
  142. data/test/instrumentation/rails_action_controller_test.rb +0 -218
  143. data/test/instrumentation/rails_action_mailer_test.rb +0 -66
  144. data/test/instrumentation/rails_action_view_test.rb +0 -154
  145. data/test/instrumentation/rails_active_job_test.rb +0 -65
  146. data/test/instrumentation/rails_active_record_database_missing_test.rb +0 -44
  147. data/test/instrumentation/rails_active_record_test.rb +0 -116
  148. data/test/instrumentation/redis_test.rb +0 -152
  149. data/test/instrumentation/resque_test.rb +0 -188
  150. data/test/instrumentation/rest_client_test.rb +0 -106
  151. data/test/instrumentation/sequel_test.rb +0 -111
  152. data/test/instrumentation/shoryuken_test.rb +0 -47
  153. data/test/instrumentation/sidekiq-client_test.rb +0 -169
  154. data/test/instrumentation/sidekiq-worker_test.rb +0 -180
  155. data/test/secrets_test.rb +0 -112
  156. data/test/serverless_test.rb +0 -369
  157. data/test/snapshot/deltable_test.rb +0 -17
  158. data/test/snapshot/docker_container_test.rb +0 -82
  159. data/test/snapshot/fargate_container_test.rb +0 -82
  160. data/test/snapshot/fargate_process_test.rb +0 -35
  161. data/test/snapshot/fargate_task_test.rb +0 -49
  162. data/test/snapshot/google_cloud_run_instance_test.rb +0 -74
  163. data/test/snapshot/google_cloud_run_process_test.rb +0 -33
  164. data/test/snapshot/lambda_function_test.rb +0 -37
  165. data/test/snapshot/ruby_process_test.rb +0 -32
  166. data/test/support/apps/active_record/active_record.rb +0 -24
  167. data/test/support/apps/grpc/boot.rb +0 -23
  168. data/test/support/apps/grpc/grpc_server.rb +0 -84
  169. data/test/support/apps/http_endpoint/boot.rb +0 -28
  170. data/test/support/apps/rails/boot.rb +0 -219
  171. data/test/support/apps/rails/models/block.rb +0 -21
  172. data/test/support/apps/rails/models/block6.rb +0 -21
  173. data/test/support/apps/resque/boot.rb +0 -5
  174. data/test/support/apps/resque/jobs/resque_error_job.rb +0 -22
  175. data/test/support/apps/resque/jobs/resque_fast_job.rb +0 -23
  176. data/test/support/apps/sidekiq/boot.rb +0 -25
  177. data/test/support/apps/sidekiq/jobs/sidekiq_job_1.rb +0 -9
  178. data/test/support/apps/sidekiq/jobs/sidekiq_job_2.rb +0 -10
  179. data/test/support/apps/sidekiq/worker.rb +0 -37
  180. data/test/support/helpers.rb +0 -85
  181. data/test/support/mock_timer.rb +0 -20
  182. data/test/test_helper.rb +0 -69
  183. data/test/trace/custom_test.rb +0 -233
  184. data/test/trace/id_management_test.rb +0 -78
  185. data/test/trace/instrumented_logger_test.rb +0 -39
  186. data/test/trace/processor_test.rb +0 -58
  187. data/test/trace/span_context_test.rb +0 -22
  188. data/test/trace/span_test.rb +0 -179
  189. data/test/trace/tracer_async_test.rb +0 -243
  190. data/test/trace/tracer_provider_test.rb +0 -148
  191. data/test/trace/tracer_test.rb +0 -363
  192. data/test/util_test.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35cfa89077e641cdd3e6fb0a97008f1e10ad7b703dc5b2b79e90a383dfc30099
4
- data.tar.gz: dd9b39744a6432f619e496c759da84bffdfe114824bef0bbbcea155dbef3a284
3
+ metadata.gz: 4a940d6791ca18a45f73ca949de0268cb4195ca269991d78683d1d89c3801919
4
+ data.tar.gz: c4a79bc94556b873ca0d2cb3cbe2b988914e2579d6fd124bbf4925142316dd85
5
5
  SHA512:
6
- metadata.gz: 47d8317281da1f8ae6adb6f740055d4845f125da6ccac65ce03ff68e4efc9a1eb22530f62571ef12bad952866832d99076b1ba4d62a6c876d0aaf4ab3b9c6ae5
7
- data.tar.gz: bc463f59a0662b0fe8ff5e34996631b1ed01215ef22e8f16781e7a249936e602959c1fecba74a3fe42e0c718f0ed37a4c62aa732c3b0f37f92cb49d9472e1e6f
6
+ metadata.gz: f0c11ead2a6eb5e7cf4811a44b0edb6af0edbed588e712a080b5de07a704fbd28b91474e593b6342537459fe69cc0fcc60dfb83c18618e8e4ca5babea5096d6b
7
+ data.tar.gz: 5a22a83d6a6f930df189a914116398b372976d95f76ce86ae63b18c2f014eb4aa170a5415f5b6feb2f69b63bc9aff511d16e2d5e5bfb2682c369a0299cb5d2d3
@@ -122,6 +122,7 @@ module Instana
122
122
  long_instana_id: long_instana_id? ? sanitized_t : nil,
123
123
  external_trace_id: external_trace_id,
124
124
  external_state: @env['HTTP_TRACESTATE'],
125
+ external_trace_flags: context_from_trace_parent[:external_trace_flags],
125
126
  from_w3c: false
126
127
  }.reject { |_, v| v.nil? }
127
128
  end
@@ -140,6 +141,7 @@ module Instana
140
141
  external_state: @env['HTTP_TRACESTATE'],
141
142
  trace_id: trace_id,
142
143
  span_id: span_id,
144
+ external_trace_flags: matches['flags'],
143
145
  from_w3c: true
144
146
  }
145
147
  end
@@ -56,7 +56,8 @@ module Instana
56
56
  # without a backtrace (no exception)
57
57
  current_span.record_exception(nil)
58
58
  end
59
-
59
+ extra_headers = ::Instana::Util.extra_header_tags(response)&.merge(::Instana::Util.extra_header_tags(request))
60
+ kv_payload[:http][:header] = extra_headers unless extra_headers&.empty?
60
61
  response
61
62
  rescue => e
62
63
  current_span&.record_exception(e)
@@ -10,7 +10,7 @@ module Instana
10
10
  @app = app
11
11
  end
12
12
 
13
- def call(env) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
13
+ def call(env)
14
14
  req = InstrumentedRequest.new(env)
15
15
  kvs = {
16
16
  http: req.request_tags
@@ -26,75 +26,14 @@ module Instana
26
26
  @trace_token = OpenTelemetry::Context.attach(trace_ctx)
27
27
  status, headers, response = @app.call(env)
28
28
 
29
- if ::Instana.tracer.tracing?
30
- unless req.correlation_data.empty?
31
- current_span[:crid] = req.correlation_data[:id]
32
- current_span[:crtp] = req.correlation_data[:type]
33
- end
34
-
35
- if !req.instana_ancestor.empty? && req.continuing_from_trace_parent?
36
- current_span[:ia] = req.instana_ancestor
37
- end
38
-
39
- if req.continuing_from_trace_parent?
40
- current_span[:tp] = true
41
- end
42
-
43
- if req.external_trace_id?
44
- current_span[:lt] = req.external_trace_id
45
- end
46
-
47
- if req.synthetic?
48
- current_span[:sy] = true
49
- end
50
-
51
- # In case some previous middleware returned a string status, make sure that we're dealing with
52
- # an integer. In Ruby nil.to_i, "asdfasdf".to_i will always return 0 from Ruby versions 1.8.7 and newer.
53
- # So if an 0 status is reported here, it indicates some other issue (e.g. no status from previous middleware)
54
- # See Rack Spec: https://www.rubydoc.info/github/rack/rack/file/SPEC#label-The+Status
55
- kvs[:http][:status] = status.to_i
56
-
57
- if status.to_i >= 500
58
- # Because of the 5xx response, we flag this span as errored but
59
- # without a backtrace (no exception)
60
- ::Instana.tracer.log_error(nil)
61
- end
62
-
63
- # If the framework instrumentation provides a path template,
64
- # pass it into the span here.
65
- # See: https://www.instana.com/docs/tracing/custom-best-practices/#path-templates-visual-grouping-of-http-endpoints
66
- kvs[:http][:path_tpl] = env['INSTANA_HTTP_PATH_TEMPLATE'] if env['INSTANA_HTTP_PATH_TEMPLATE']
67
-
68
- # Save the span context before the trace ends so we can place
69
- # them in the response headers in the ensure block
70
- trace_context = ::Instana.tracer.current_span.context
71
- end
72
-
29
+ trace_context = process_span_tags(req, current_span, kvs, status, env) if ::Instana.tracer.tracing?
30
+ merge_response_headers(kvs, headers)
73
31
  [status, headers, response]
74
32
  rescue Exception => e
75
33
  current_span.record_exception(e) if ::Instana.tracer.tracing?
76
34
  raise
77
35
  ensure
78
- if ::Instana.tracer.tracing?
79
- if headers
80
- # Set response headers; encode as hex string
81
- if trace_context.active?
82
- headers['X-Instana-T'] = trace_context.trace_id_header
83
- headers['X-Instana-S'] = trace_context.span_id_header
84
- headers['X-Instana-L'] = '1'
85
-
86
- headers['Tracestate'] = trace_context.trace_state_header
87
- else
88
- headers['X-Instana-L'] = '0'
89
- end
90
-
91
- headers['Traceparent'] = trace_context.trace_parent_header
92
- headers['Server-Timing'] = "intid;desc=#{trace_context.trace_id_header}"
93
- end
94
- current_span.set_tags(kvs)
95
- OpenTelemetry::Context.detach(@trace_token) if @trace_token
96
- current_span.finish
97
- end
36
+ finalize_trace(current_span, kvs, headers, trace_context) if ::Instana.tracer.tracing?
98
37
  end
99
38
 
100
39
  private
@@ -112,7 +51,8 @@ module Instana
112
51
  level: incoming_context[:level],
113
52
  baggage: {
114
53
  external_trace_id: incoming_context[:external_trace_id],
115
- external_state: incoming_context[:external_state]
54
+ external_state: incoming_context[:external_state],
55
+ external_trace_flags: incoming_context[:external_trace_flags]
116
56
  }
117
57
  )
118
58
  end
@@ -121,5 +61,85 @@ module Instana
121
61
  end
122
62
  parent_context
123
63
  end
64
+
65
+ def process_span_tags(req, current_span, kvs, status, env)
66
+ add_correlation_data(req, current_span)
67
+ add_trace_parent_data(req, current_span)
68
+ add_status_and_error(kvs, status)
69
+ add_path_template(kvs, env)
70
+
71
+ # Save the span context before the trace ends so we can place
72
+ # them in the response headers in the ensure block
73
+ ::Instana.tracer.current_span.context
74
+ end
75
+
76
+ def add_correlation_data(req, current_span)
77
+ return if req.correlation_data.empty?
78
+
79
+ current_span[:crid] = req.correlation_data[:id]
80
+ current_span[:crtp] = req.correlation_data[:type]
81
+ end
82
+
83
+ def add_trace_parent_data(req, current_span)
84
+ if !req.instana_ancestor.empty? && req.continuing_from_trace_parent?
85
+ current_span[:ia] = req.instana_ancestor
86
+ end
87
+
88
+ current_span[:tp] = true if req.continuing_from_trace_parent?
89
+ current_span[:lt] = req.external_trace_id if req.external_trace_id?
90
+ current_span[:sy] = true if req.synthetic?
91
+ end
92
+
93
+ def add_status_and_error(kvs, status)
94
+ # In case some previous middleware returned a string status, make sure that we're dealing with
95
+ # an integer. In Ruby nil.to_i, "asdfasdf".to_i will always return 0 from Ruby versions 1.8.7 and newer.
96
+ # So if an 0 status is reported here, it indicates some other issue (e.g. no status from previous middleware)
97
+ # See Rack Spec: https://www.rubydoc.info/github/rack/rack/file/SPEC#label-The+Status
98
+ kvs[:http][:status] = status.to_i
99
+
100
+ return unless status.to_i >= 500
101
+
102
+ # Because of the 5xx response, we flag this span as errored but
103
+ # without a backtrace (no exception)
104
+ ::Instana.tracer.log_error(nil)
105
+ end
106
+
107
+ def add_path_template(kvs, env)
108
+ # If the framework instrumentation provides a path template,
109
+ # pass it into the span here.
110
+ # See: https://www.instana.com/docs/tracing/custom-best-practices/#path-templates-visual-grouping-of-http-endpoints
111
+ kvs[:http][:path_tpl] = env['INSTANA_HTTP_PATH_TEMPLATE'] if env['INSTANA_HTTP_PATH_TEMPLATE']
112
+ end
113
+
114
+ def merge_response_headers(kvs, headers)
115
+ extra_response_headers = ::Instana::Util.extra_header_tags(headers)
116
+ if kvs[:http][:header].nil?
117
+ kvs[:http][:header] = extra_response_headers
118
+ else
119
+ kvs[:http][:header].merge!(extra_response_headers)
120
+ end
121
+ end
122
+
123
+ def finalize_trace(current_span, kvs, headers, trace_context)
124
+ set_response_headers(headers, trace_context) if headers
125
+ current_span.set_tags(kvs)
126
+ OpenTelemetry::Context.detach(@trace_token) if @trace_token
127
+ current_span.finish
128
+ end
129
+
130
+ def set_response_headers(headers, trace_context)
131
+ # Set response headers; encode as hex string
132
+ if trace_context.active?
133
+ headers['X-Instana-T'] = trace_context.trace_id_header
134
+ headers['X-Instana-S'] = trace_context.span_id_header
135
+ headers['X-Instana-L'] = '1'
136
+ headers['Tracestate'] = trace_context.trace_state_header
137
+ else
138
+ headers['X-Instana-L'] = '0'
139
+ end
140
+
141
+ headers['Traceparent'] = trace_context.trace_parent_header
142
+ headers['Server-Timing'] = "intid;desc=#{trace_context.trace_id_header}"
143
+ end
124
144
  end
125
145
  end
data/lib/instana/setup.rb CHANGED
@@ -41,6 +41,7 @@ require 'instana/backend/serverless_agent'
41
41
  require 'instana/backend/agent'
42
42
  require 'instana/trace'
43
43
  require 'instana/trace/tracer_provider'
44
+ require 'instana/span_filtering'
44
45
 
45
46
  ::Instana.setup
46
47
  ::Instana.agent.setup
@@ -0,0 +1,134 @@
1
+ # (c) Copyright IBM Corp. 2025
2
+
3
+ module Instana
4
+ module SpanFiltering
5
+ # Represents a condition for filtering spans
6
+ #
7
+ # A condition consists of:
8
+ # - key: The attribute to match against (category, kind, type, or span attribute)
9
+ # - values: List of values to match against (OR logic between values)
10
+ # - match_type: String matching strategy (strict, startswith, endswith, contains)
11
+ class Condition
12
+ attr_reader :key, :values, :match_type
13
+
14
+ def initialize(key, values, match_type = 'strict')
15
+ @key = key
16
+ @values = Array(values)
17
+ @match_type = match_type
18
+ end
19
+
20
+ # Check if a span matches this condition
21
+ # @param span [Hash] The span to check
22
+ # @return [Boolean] True if the span matches any of the values
23
+ def matches?(span)
24
+ attribute_value = extract_attribute(span, @key)
25
+ return false if attribute_value.nil?
26
+
27
+ @values.any? { |value| matches_value?(attribute_value, value) }
28
+ end
29
+
30
+ private
31
+
32
+ # Extract an attribute from a span
33
+ # @param span [Hash] The span to extract from
34
+ # @param key [String] The key to extract
35
+ # @return [Object, nil] The attribute value or nil if not found
36
+ def extract_attribute(span, key)
37
+ case key
38
+ when 'category'
39
+ # Map to appropriate span attribute for category
40
+ determine_category(span)
41
+ when 'kind'
42
+ # Map to appropriate span attribute for kind
43
+ span[:k] || span['k']
44
+ when 'type'
45
+ # Map to appropriate span attribute for type
46
+ span[:n] || span['n']
47
+ else
48
+ # Handle nested attributes with dot notation
49
+ extract_nested_attribute(span[:data] || span['data'], key)
50
+ end
51
+ end
52
+
53
+ # Determine the category of a span based on its properties
54
+ # @param span [Hash] The span to categorize
55
+ # @return [String, nil] The category or nil if not determinable
56
+ def determine_category(span)
57
+ data = span[:data] || span['data']
58
+ return nil unless data
59
+
60
+ if data[:http] || data['http']
61
+ 'protocols'
62
+ elsif data[:redis] || data[:mysql] || data[:pg] || data[:db]
63
+ 'databases'
64
+ elsif data[:sqs] || data[:sns] || data[:mq]
65
+ 'messaging'
66
+ elsif (span[:n] || span['n'])&.start_with?('log.')
67
+ 'logging'
68
+ end
69
+ end
70
+
71
+ # Extract a nested attribute using dot notation
72
+ # @param data [Hash] The data hash to extract from
73
+ # @param key [String] The key in dot notation
74
+ # @return [Object, nil] The attribute value or nil if not found
75
+ def extract_nested_attribute(data, key)
76
+ return nil unless data
77
+
78
+ parts = key.split('.')
79
+ current = data
80
+
81
+ parts.each do |part|
82
+ # Try symbol key first, then string key
83
+ if current.key?(part.to_sym)
84
+ current = current[part.to_sym]
85
+ elsif current.key?(part)
86
+ current = current[part]
87
+ else
88
+ return nil # Key not found
89
+ end
90
+
91
+ # Only return nil if the value is actually nil, not for false values
92
+ end
93
+
94
+ current
95
+ end
96
+
97
+ # Check if an attribute value matches a condition value
98
+ # @param attribute_value [Object] The attribute value
99
+ # @param condition_value [String] The condition value
100
+ # @return [Boolean] True if the attribute value matches the condition value
101
+ def matches_value?(attribute_value, condition_value)
102
+ # Handle wildcard
103
+ return true if condition_value == '*'
104
+
105
+ # Direct comparison first - this should handle boolean values correctly
106
+ return true if attribute_value == condition_value
107
+
108
+ # For strict matching with type conversion
109
+ if @match_type == 'strict'
110
+ # Convert to strings and compare
111
+ attribute_str = attribute_value.to_s
112
+ condition_str = condition_value.to_s
113
+ return attribute_str == condition_str
114
+ end
115
+
116
+ # For other match types, convert both to strings
117
+ attribute_str = attribute_value.to_s
118
+ condition_str = condition_value.to_s
119
+
120
+ case @match_type
121
+ when 'startswith'
122
+ attribute_str.start_with?(condition_str)
123
+ when 'endswith'
124
+ attribute_str.end_with?(condition_str)
125
+ when 'contains'
126
+ attribute_str.include?(condition_str)
127
+ else
128
+ # Default to strict matching
129
+ attribute_str == condition_str
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,262 @@
1
+ # (c) Copyright IBM Corp. 2025
2
+
3
+ require 'yaml'
4
+
5
+ module Instana
6
+ module SpanFiltering
7
+ # Configuration class for span filtering
8
+ #
9
+ # This class handles loading and managing span filtering rules from various sources:
10
+ # - YAML configuration file (via INSTANA_CONFIG_PATH)
11
+ # - Environment variables
12
+ # - Agent discovery response
13
+ # It supports both include and exclude rules with various matching strategies
14
+ class Configuration
15
+ attr_reader :include_rules, :exclude_rules, :deactivated
16
+
17
+ TRACING_CONFIG_WARNING = 'Please use "tracing" instead of "com.instana.tracing" for local configuration file.'.freeze
18
+
19
+ def initialize
20
+ @include_rules = []
21
+ @exclude_rules = []
22
+ @deactivated = false
23
+ load_configuration
24
+ end
25
+
26
+ # Load configuration from all available sources
27
+ def load_configuration
28
+ load_from_yaml
29
+ load_from_env_vars unless rules_loaded?
30
+ load_from_agent unless rules_loaded?
31
+ end
32
+
33
+ private
34
+
35
+ # Load configuration from agent discovery response
36
+ def load_from_agent
37
+ # Try to get discovery value immediately first
38
+ discovery = ::Instana.agent&.delegate&.send(:discovery_value)
39
+ if discovery && discovery.is_a?(Hash) && !discovery.empty?
40
+ process_discovery_config(discovery)
41
+ return
42
+ end
43
+
44
+ # If not available, set up a timer task to periodically check for discovery
45
+ setup_discovery_timer
46
+ rescue => e
47
+ Instana.logger.warn("Failed to load span filtering configuration from agent: #{e.message}")
48
+ end
49
+
50
+ # Set up a timer task to periodically check for discovery
51
+ def setup_discovery_timer
52
+ # Don't create a timer task if we're in a test environment
53
+ return if ENV.key?('INSTANA_TEST')
54
+
55
+ # Create a timer task that checks for discovery every second
56
+ @discovery_timer = Concurrent::TimerTask.new(execution_interval: 1) do
57
+ check_discovery
58
+ end
59
+
60
+ # Start the timer task
61
+ @discovery_timer.execute
62
+ end
63
+
64
+ # Check if discovery is available and process it
65
+ def check_discovery
66
+ discovery = ::Instana.agent&.delegate.send(:discovery_value)
67
+ if discovery && discovery.is_a?(Hash) && !discovery.empty?
68
+ process_discovery_config(discovery)
69
+
70
+ # Shutdown the timer task after successful processing
71
+ @discovery_timer.shutdown if @discovery_timer
72
+
73
+ return true
74
+ end
75
+
76
+ false
77
+ rescue => e
78
+ Instana.logger.warn("Error checking discovery in timer task: #{e.message}")
79
+ false
80
+ end
81
+
82
+ # Process the discovery configuration
83
+ def process_discovery_config(discovery)
84
+ # Check if tracing configuration exists in the discovery response
85
+ tracing_config = discovery['tracing']
86
+ return unless tracing_config
87
+
88
+ # Process filter configuration
89
+ if tracing_config['filter']
90
+ filter_config = tracing_config['filter']
91
+ @deactivated = filter_config['deactivate'] == true
92
+
93
+ # Process include rules
94
+ process_rules(filter_config['include'], true) if filter_config['include']
95
+
96
+ # Process exclude rules
97
+ process_rules(filter_config['exclude'], false) if filter_config['exclude']
98
+ end
99
+
100
+ # Process disable configuration
101
+ if tracing_config['disable']
102
+ process_disable_config(tracing_config['disable'])
103
+ end
104
+
105
+ # Return true to indicate successful processing
106
+ true
107
+ rescue => e
108
+ Instana.logger.warn("Failed to process discovery configuration: #{e.message}")
109
+ false
110
+ end
111
+
112
+ # Check if the rules are already loaded
113
+ def rules_loaded?
114
+ @include_rules.any? || @exclude_rules.any?
115
+ end
116
+
117
+ # Load configuration from YAML file specified by INSTANA_CONFIG_PATH
118
+ def load_from_yaml
119
+ config_path = ENV['INSTANA_CONFIG_PATH']
120
+ return unless config_path && File.exist?(config_path)
121
+
122
+ begin
123
+ yaml_content = YAML.safe_load(File.read(config_path))
124
+
125
+ # Support both "tracing" and "com.instana.tracing" as top-level keys
126
+ tracing_config = yaml_content['tracing'] || yaml_content['com.instana.tracing']
127
+ ::Instana.logger.warn(TRACING_CONFIG_WARNING) if yaml_content.key?('com.instana.tracing')
128
+ return unless tracing_config
129
+
130
+ # Process filter configuration
131
+ if tracing_config['filter']
132
+ filter_config = tracing_config['filter']
133
+ @deactivated = filter_config['deactivate'] == true
134
+
135
+ # Process include rules
136
+ process_rules(filter_config['include'], true) if filter_config['include']
137
+
138
+ # Process exclude rules
139
+ process_rules(filter_config['exclude'], false) if filter_config['exclude']
140
+ end
141
+
142
+ # Process disable configuration
143
+ if tracing_config['disable']
144
+ process_disable_config(tracing_config['disable'])
145
+ end
146
+ rescue => e
147
+ Instana.logger.warn("Failed to load span filtering configuration from YAML: #{e.message}")
148
+ end
149
+ end
150
+
151
+ # Load configuration from environment variables
152
+ def load_from_env_vars
153
+ ENV.each do |key, value|
154
+ next unless key.start_with?('INSTANA_TRACING_FILTER_')
155
+
156
+ parts = key.split('_')
157
+ next unless parts.size >= 5
158
+
159
+ policy = parts[3].downcase
160
+ next unless ['include', 'exclude'].include?(policy)
161
+
162
+ if parts[4] == 'ATTRIBUTES'
163
+ process_env_attributes(policy, parts[4..].join('_'), value)
164
+ elsif policy == 'exclude' && parts[4] == 'SUPPRESSION'
165
+ process_env_suppression(parts[3..].join('_'), value)
166
+ end
167
+ end
168
+
169
+ return unless !ENV["INSTANA_TRACING_DISABLE"].nil? && !%w[True true 1].include?(ENV["INSTANA_TRACING_DISABLE"])
170
+
171
+ process_disable_config(ENV["INSTANA_TRACING_DISABLE"].split(','))
172
+ end
173
+
174
+ # Process rules from YAML configuration
175
+ def process_rules(rules_config, is_include)
176
+ rules_config.each do |rule_config|
177
+ name = rule_config['name']
178
+ suppression = is_include ? false : (rule_config['suppression'] != false) # Default true for exclude
179
+
180
+ conditions = []
181
+ rule_config['attributes'].each do |attr_config|
182
+ key = attr_config['key']
183
+ values = attr_config['values']
184
+ match_type = attr_config['match_type'] || 'strict'
185
+
186
+ conditions << Condition.new(key, values, match_type)
187
+ end
188
+
189
+ rule = FilterRule.new(name, suppression, conditions)
190
+ is_include ? @include_rules << rule : @exclude_rules << rule
191
+ end
192
+ end
193
+
194
+ # Process attributes from environment variables
195
+ def process_env_attributes(policy, name, value)
196
+ # Parse rules from environment variable format
197
+ # Format: key;values;match_type|key;values;match_type
198
+ rules = value.split('|')
199
+ conditions = []
200
+
201
+ rules.each do |rule|
202
+ parts = rule.split(';')
203
+ next unless parts.size >= 2
204
+
205
+ key = parts[0]
206
+ values = parts[1].split(',')
207
+ match_type = parts[2] || 'strict'
208
+
209
+ conditions << Condition.new(key, values, match_type)
210
+ end
211
+
212
+ rule_name = "EnvRule_#{name}"
213
+ suppression = policy == 'exclude' # Default true for exclude
214
+
215
+ rule = FilterRule.new(rule_name, suppression, conditions)
216
+ policy == 'include' ? @include_rules << rule : @exclude_rules << rule
217
+ end
218
+
219
+ # Process suppression setting from environment variables
220
+ def process_env_suppression(policy_name, value)
221
+ # Find the corresponding rule and update its suppression value
222
+ rule_index = policy_name.split('_')[1].to_i
223
+ return if rule_index >= @exclude_rules.size
224
+
225
+ suppression = %w[1 true True].include?(value)
226
+ @exclude_rules[rule_index].suppression = suppression
227
+ end
228
+
229
+ # Process disable configuration from YAML or agent discovery
230
+ # @param disable_config [Array] The disable configuration array
231
+ def process_disable_config(disable_config)
232
+ return unless disable_config.is_a?(Array)
233
+
234
+ disable_config.each do |item|
235
+ if item.is_a?(Hash)
236
+ item.each do |key, value|
237
+ if value == true
238
+ update_instana_config_for_disabled_technology(key)
239
+ end
240
+ end
241
+ elsif item.is_a?(String)
242
+ update_instana_config_for_disabled_technology(item)
243
+ end
244
+ end
245
+ end
246
+
247
+ # Update Instana::Config for a disabled technology
248
+ # @param technology [String] The technology to disable
249
+ def update_instana_config_for_disabled_technology(technology)
250
+ tech_sym = technology.to_sym
251
+
252
+ case tech_sym
253
+ when :redis
254
+ ::Instana.config[:redis][:enabled] = false
255
+ when :databases
256
+ # If databases category is disabled, also disable redis
257
+ ::Instana.config[:redis][:enabled] = false
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # (c) Copyright IBM Corp. 2025
4
+
5
+ module Instana
6
+ module SpanFiltering
7
+ # Represents a filtering rule for spans
8
+ #
9
+ # A rule consists of:
10
+ # - name: A human-readable identifier for the rule
11
+ # - suppression: Whether child spans should be suppressed (only for exclude rules)
12
+ # - conditions: A list of conditions that must all be satisfied (AND logic)
13
+ class FilterRule
14
+ attr_reader :name
15
+ attr_accessor :suppression, :conditions
16
+
17
+ def initialize(name, suppression, conditions)
18
+ @name = name
19
+ @suppression = suppression
20
+ @conditions = conditions
21
+ end
22
+
23
+ # Check if a span matches this rule
24
+ # @param span [Hash] The span to check
25
+ # @return [Boolean] True if the span matches all conditions
26
+ def matches?(span)
27
+ @conditions.all? { |condition| condition.matches?(span) }
28
+ end
29
+ end
30
+ end
31
+ end