opentracing-instrumentation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.BUNDLER_VERSION +1 -0
  3. data/.drone.jsonnet +35 -0
  4. data/.gitignore +1 -0
  5. data/.gitlab-ci.yml +80 -0
  6. data/.rubocop.yml +36 -0
  7. data/.ruby-version +1 -0
  8. data/GEM_VERSION +1 -0
  9. data/Gemfile +19 -0
  10. data/Gemfile.lock +101 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +39 -0
  13. data/Rakefile +11 -0
  14. data/bin/console +16 -0
  15. data/bin/setup +8 -0
  16. data/lib/opentracing/instrumentation.rb +23 -0
  17. data/lib/opentracing/instrumentation/common.rb +13 -0
  18. data/lib/opentracing/instrumentation/common/error_writer.rb +56 -0
  19. data/lib/opentracing/instrumentation/faraday.rb +10 -0
  20. data/lib/opentracing/instrumentation/faraday/response_logger.rb +76 -0
  21. data/lib/opentracing/instrumentation/faraday/trace_middleware.rb +202 -0
  22. data/lib/opentracing/instrumentation/mongo.rb +12 -0
  23. data/lib/opentracing/instrumentation/mongo/direct_sanitazer.rb +16 -0
  24. data/lib/opentracing/instrumentation/mongo/query_sanitazer.rb +84 -0
  25. data/lib/opentracing/instrumentation/mongo/trace_subscriber.rb +107 -0
  26. data/lib/opentracing/instrumentation/object_wrapper.rb +59 -0
  27. data/lib/opentracing/instrumentation/rack.rb +11 -0
  28. data/lib/opentracing/instrumentation/rack/http_tagger.rb +69 -0
  29. data/lib/opentracing/instrumentation/rack/trace_middleware.rb +94 -0
  30. data/lib/opentracing/instrumentation/redis.rb +18 -0
  31. data/lib/opentracing/instrumentation/redis/config.rb +40 -0
  32. data/lib/opentracing/instrumentation/redis/span_builder.rb +85 -0
  33. data/lib/opentracing/instrumentation/redis/tracing_driver_wrapper.rb +117 -0
  34. data/lib/opentracing/instrumentation/sidekiq.rb +17 -0
  35. data/lib/opentracing/instrumentation/sidekiq/client_middleware.rb +66 -0
  36. data/lib/opentracing/instrumentation/sidekiq/job_tagger.rb +61 -0
  37. data/lib/opentracing/instrumentation/sidekiq/server_middleware.rb +70 -0
  38. data/lib/opentracing/instrumentation/sinatra.rb +11 -0
  39. data/lib/opentracing/instrumentation/sinatra/trace_middleware.rb +64 -0
  40. data/lib/opentracing/instrumentation/thrift.rb +15 -0
  41. data/lib/opentracing/instrumentation/thrift/config.rb +24 -0
  42. data/lib/opentracing/instrumentation/thrift/traced_protocol.rb +145 -0
  43. data/lib/opentracing/instrumentation/thrift/traced_protocol_factory.rb +48 -0
  44. data/lib/opentracing/instrumentation/version.rb +7 -0
  45. data/opentracing-instrumentation.gemspec +40 -0
  46. metadata +255 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'opentracing/instrumentation/mongo/query_sanitazer'
5
+
6
+ module OpenTracing
7
+ module Instrumentation
8
+ module Mongo
9
+ # TraceSubscriber trace mongo requests
10
+ class TraceSubscriber
11
+ include Forwardable
12
+
13
+ DEFAULT_OPERATION_NAME = 'mongo_command'
14
+
15
+ def initialize(
16
+ tracer: OpenTracing.global_tracer,
17
+ scope_store: {},
18
+ operation_name: DEFAULT_OPERATION_NAME,
19
+ sanitazer: QuerySanitazer.new
20
+ )
21
+ @tracer = tracer
22
+ @monitor = Monitor.new
23
+ @scope_store = scope_store
24
+ @operation_name = operation_name
25
+ @sanitazer = sanitazer
26
+ end
27
+
28
+ def started(event)
29
+ scope = tracer.start_active_span(
30
+ operation_name,
31
+ tags: base_tags(event).merge(mongo_tags(event)),
32
+ )
33
+
34
+ store_scope(event.operation_id, scope)
35
+ end
36
+
37
+ def succeeded(event)
38
+ clean_scope(event.operation_id)
39
+ end
40
+
41
+ def failed(event)
42
+ tag_error(event.operation_id, event.message, event.failure)
43
+ clean_scope(event.operation_id)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :tracer
49
+ attr_reader :scope_store
50
+ attr_reader :monitor
51
+ attr_reader :operation_name
52
+ attr_reader :sanitazer
53
+
54
+ def base_tags(event)
55
+ {
56
+ 'component' => 'db',
57
+
58
+ 'db.type' => 'mongo',
59
+ 'db.instance' => event.address.to_s,
60
+ }
61
+ end
62
+
63
+ def mongo_tags(event)
64
+ collection_name = event.command[event.command_name]
65
+ command_args = sanitazer.sanitaze(event.command, event.command_name)
66
+
67
+ {
68
+ 'mongo.request_id' => event.request_id,
69
+ 'mongo.operation_id' => event.operation_id,
70
+ 'mongo.database_name' => event.database_name,
71
+ 'mongo.collection_name' => collection_name,
72
+ 'mongo.command_name' => event.command_name,
73
+ 'mongo.command_args' => JSON.dump(command_args),
74
+ }
75
+ end
76
+
77
+ def store_scope(operation_id, scope)
78
+ monitor.synchronize do
79
+ scope_store[operation_id] = scope
80
+ end
81
+ end
82
+
83
+ def clean_scope(operation_id)
84
+ monitor.synchronize do
85
+ scope_store
86
+ .delete(operation_id)
87
+ .close
88
+ end
89
+ end
90
+
91
+ ERROR_TAG = 'error'
92
+
93
+ def tag_error(operation_id, message, object)
94
+ monitor.synchronize do
95
+ scope_store[operation_id]
96
+ .span
97
+ .set_tag(ERROR_TAG, true)
98
+ .log_kv(
99
+ 'error.kind': message,
100
+ 'error.object': JSON.dump(object),
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenTracing
4
+ module Instrumentation
5
+ # ObjectWrapper allow trace call methods on any ruby object.
6
+ #
7
+ # Usage:
8
+ # wrapped_object = OpenTracing::Instrumentation::ObjectWrapper.new(other_object)
9
+ # wrapped_object.other_object_method
10
+ class ObjectWrapper
11
+ DEFAULT_COMMAND_NAME = 'method_call'
12
+
13
+ attr_reader :tracer,
14
+ :command_name
15
+
16
+ def initialize(
17
+ object,
18
+ tracer: OpenTracing.global_tracer,
19
+ command_name: DEFAULT_COMMAND_NAME
20
+ )
21
+ @object = object
22
+ @tracer = tracer
23
+ @command_name = command_name
24
+ end
25
+
26
+ # wrapped object
27
+ attr_reader :object
28
+
29
+ def respond_to_missing?(method_name, *)
30
+ object.response_to?(method_name)
31
+ end
32
+
33
+ def method_missing(method_name, *args)
34
+ return super unless object.respond_to?(method_name)
35
+
36
+ tags = build_tags(method_name)
37
+ tracer.start_active_span(command_name, tags: tags) do |scope|
38
+ call_method(scope.span, method_name, *args)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def call_method(span, method_name, *args)
45
+ object.send(method_name, *args)
46
+ rescue StandardError => e
47
+ span.set_tag('error', true)
48
+ raise e
49
+ end
50
+
51
+ def build_tags(method)
52
+ {
53
+ 'object.class' => object.class.to_s,
54
+ 'object.method' => method.to_s,
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenTracing
4
+ module Instrumentation
5
+ # Rack tracing middlewares
6
+ module Rack
7
+ autoload :HttpTagger, 'opentracing/instrumentation/rack/http_tagger'
8
+ autoload :TraceMiddleware, 'opentracing/instrumentation/rack/trace_middleware'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ module OpenTracing
6
+ module Instrumentation
7
+ module Rack
8
+ # HttpTagger create addition tags on span
9
+ class HttpTagger
10
+ DEFAULT_TAG_REQUEST_HEADERS = {
11
+ connection: 'Connection',
12
+ content_type: 'Content-Type',
13
+ user_agent: 'User-Agent',
14
+ }.freeze
15
+
16
+ DEFAULT_TAG_RESPONSE_HEADERS = {
17
+ connection: 'Connection',
18
+ content_type: 'Content-Type',
19
+ keep_alive: 'Keep-Alive',
20
+ }.freeze
21
+
22
+ def initialize(
23
+ tag_request_headers: DEFAULT_TAG_REQUEST_HEADERS,
24
+ tag_response_headers: DEFAULT_TAG_RESPONSE_HEADERS
25
+ )
26
+ @tag_request_headers =
27
+ prepare_request_mapping(tag_request_headers)
28
+ @tag_response_headers =
29
+ prepare_response_mapping(tag_response_headers)
30
+ end
31
+
32
+ def request_tags(env)
33
+ @tag_request_headers
34
+ .transform_values { |header_name| env[header_name] }
35
+ .compact
36
+ end
37
+
38
+ def response_tags(headers)
39
+ header_mapper = lambda do |header_regexp|
40
+ headers.find do |(header, _value)|
41
+ header_regexp.match?(header)
42
+ end&.dig(1)
43
+ end
44
+ @tag_response_headers
45
+ .transform_values(&header_mapper)
46
+ .compact
47
+ end
48
+
49
+ private
50
+
51
+ def prepare_request_mapping(source_mapping)
52
+ source_mapping.map do |key, header|
53
+ rack_header = 'HTTP_' + header.tr('-', '_').upcase
54
+ tag_name = 'http.request.' + key.to_s
55
+ [tag_name, rack_header]
56
+ end.to_h
57
+ end
58
+
59
+ def prepare_response_mapping(source_mapping)
60
+ source_mapping.map do |key, header|
61
+ tag_name = 'http.response.' + key.to_s
62
+ header_regexp = /^#{header}$/i
63
+ [tag_name, header_regexp]
64
+ end.to_h
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ module OpenTracing
6
+ module Instrumentation
7
+ module Rack
8
+ # TraceMiddleware observer rack requests.
9
+ class TraceMiddleware
10
+ def initialize(
11
+ app,
12
+ logger: nil,
13
+ command_name: 'rack',
14
+ http_tagger: HttpTagger.new,
15
+ tracer: OpenTracing.global_tracer
16
+ )
17
+ @app = app
18
+ @logger = logger
19
+ @command_name = command_name
20
+ @http_tagger = http_tagger
21
+ @tracer = tracer
22
+ end
23
+
24
+ def call(env)
25
+ trace_request(env) do |span|
26
+ catch_error(span) do
27
+ status, headers, body = app.call(env)
28
+
29
+ set_response_tags(span, status, headers)
30
+ [status, headers, body]
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :app
38
+ attr_reader :http_tagger
39
+ attr_reader :tracer
40
+ attr_reader :logger
41
+
42
+ def trace_request(env)
43
+ extracted_ctx = tracer.extract(OpenTracing::FORMAT_RACK, env)
44
+ logger&.info('Tracing context extracted') if extracted_ctx
45
+ tracer.start_active_span(
46
+ @command_name,
47
+ child_of: extracted_ctx,
48
+ tags: request_tags(env),
49
+ ) do |scope|
50
+ yield(scope.span)
51
+ end
52
+ end
53
+
54
+ def catch_error(span)
55
+ yield
56
+ rescue StandardError => e
57
+ logger&.error(e)
58
+ error_tag(span, e)
59
+ raise
60
+ end
61
+
62
+ REQUEST_URI = 'REQUEST_URI'
63
+
64
+ def request_tags(env)
65
+ {
66
+ 'http.method' => env[::Rack::REQUEST_METHOD],
67
+ 'http.url' => env[REQUEST_URI],
68
+ 'span.kind' => 'server',
69
+ }.merge(http_tagger.request_tags(env))
70
+ end
71
+
72
+ def set_response_tags(span, status, headers)
73
+ set_status_tag(span, status)
74
+ set_header_tags(span, headers)
75
+ end
76
+
77
+ def set_status_tag(span, status)
78
+ span.set_tag('http.status', status)
79
+ end
80
+
81
+ def set_header_tags(span, headers)
82
+ http_tagger
83
+ .response_tags(headers)
84
+ .each(&span.method(:set_tag))
85
+ end
86
+
87
+ def error_tag(span, error)
88
+ span.set_tag('error', true)
89
+ span.log_kv('error.kind': error.class.to_s)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenTracing
4
+ module Instrumentation
5
+ # Redis tracing drivers
6
+ module Redis
7
+ EVENT_WRITE = 'redis_write'
8
+ EVENT_READ = 'redis_read'
9
+
10
+ autoload :Config,
11
+ 'opentracing/instrumentation/redis/config'
12
+ autoload :SpanBuilder,
13
+ 'opentracing/instrumentation/redis/span_builder'
14
+ autoload :TracingDriverWrapper,
15
+ 'opentracing/instrumentation/redis/tracing_driver_wrapper'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/connection/ruby'
4
+
5
+ module OpenTracing
6
+ module Instrumentation
7
+ module Redis
8
+ # Redis tracing mixin config
9
+ class Config
10
+ DEFAULT_SPAN_NAME = 'redis_command'
11
+ # Safe by default
12
+ DEFAULT_LOG_ARGS = false
13
+ DEFAULT_LOG_REPLY = false
14
+ DEFAULT_COMPONENT = 'kv'
15
+
16
+ attr_accessor :tracer
17
+ attr_accessor :span_name
18
+ attr_accessor :log_args
19
+ attr_accessor :log_reply
20
+ attr_accessor :component
21
+
22
+ def initialize(
23
+ tracer: OpenTracing.global_tracer,
24
+ span_name: DEFAULT_SPAN_NAME,
25
+ log_args: DEFAULT_LOG_ARGS,
26
+ log_reply: DEFAULT_LOG_REPLY,
27
+ component: DEFAULT_COMPONENT
28
+ )
29
+ @tracer = tracer
30
+ @span_name = span_name
31
+ @log_args = log_args
32
+ @log_reply = log_reply
33
+ @component = component
34
+
35
+ yield self if block_given?
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OpenTracing
6
+ module Instrumentation
7
+ module Redis
8
+ # SpanBuilder create span with tags and logs
9
+ class SpanBuilder
10
+ extend Forwardable
11
+
12
+ def initialize(
13
+ config: Config.new,
14
+ error_writer: Common::ErrorWriter.new
15
+ )
16
+ @config = config
17
+ @error_writer = error_writer
18
+ end
19
+
20
+ def_delegators :@config,
21
+ :tracer,
22
+ :span_name,
23
+ :component,
24
+ :log_args,
25
+ :log_reply
26
+
27
+ def_delegators :@error_writer,
28
+ :write_error
29
+
30
+ def start_active_scope(connection_class, peer_addr)
31
+ tags = build_tags(connection_class, peer_addr)
32
+ tracer.start_active_span(
33
+ span_name,
34
+ tags: tags,
35
+ )
36
+ end
37
+
38
+ def build_tags(connection_class, peer_addr)
39
+ {
40
+ 'span.kind' => 'client',
41
+ 'component' => component,
42
+ 'peer.address' => peer_addr,
43
+ 'peer.service' => 'redis',
44
+ 'redis.driver' => connection_class.to_s,
45
+ }.compact
46
+ end
47
+
48
+ REDACTED_ARG = ['*'].freeze
49
+
50
+ def write_log_command(span, command)
51
+ command_name, *args = command
52
+ args_value = log_args ? JSON.dump(args) : REDACTED_ARG * args.size
53
+
54
+ span.log_kv(
55
+ event: EVENT_WRITE,
56
+ 'redis.command': command_name,
57
+ 'redis.args': args_value,
58
+ )
59
+ end
60
+
61
+ def write_error_reply(span, reply)
62
+ return unless reply.is_a?(::Redis::CommandError)
63
+
64
+ span.set_tag('error', true)
65
+ span.log_kv(
66
+ 'error.kind': 'redis_error',
67
+ message: reply.to_s,
68
+ event: EVENT_READ,
69
+ )
70
+ end
71
+
72
+ def write_log_reply(span, reply)
73
+ write_error_reply(span, reply)
74
+
75
+ return unless log_reply
76
+
77
+ span.log_kv(
78
+ event: EVENT_READ,
79
+ 'redis.reply': JSON.dump(reply),
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end