datadog-ci 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/LICENSE-3rdparty.csv +1 -0
  4. data/README.md +64 -0
  5. data/lib/datadog/ci/configuration/components.rb +51 -5
  6. data/lib/datadog/ci/configuration/settings.rb +36 -8
  7. data/lib/datadog/ci/contrib/cucumber/configuration/settings.rb +2 -3
  8. data/lib/datadog/ci/contrib/cucumber/formatter.rb +8 -10
  9. data/lib/datadog/ci/contrib/cucumber/integration.rb +3 -5
  10. data/lib/datadog/ci/contrib/integration.rb +149 -0
  11. data/lib/datadog/ci/contrib/minitest/configuration/settings.rb +2 -3
  12. data/lib/datadog/ci/contrib/minitest/hooks.rb +8 -4
  13. data/lib/datadog/ci/contrib/minitest/integration.rb +3 -5
  14. data/lib/datadog/ci/contrib/rspec/configuration/settings.rb +2 -3
  15. data/lib/datadog/ci/contrib/rspec/example.rb +5 -8
  16. data/lib/datadog/ci/contrib/rspec/integration.rb +3 -5
  17. data/lib/datadog/ci/contrib/settings.rb +33 -0
  18. data/lib/datadog/ci/ext/environment/providers/local_git.rb +7 -0
  19. data/lib/datadog/ci/ext/settings.rb +2 -0
  20. data/lib/datadog/ci/ext/transport.rb +19 -0
  21. data/lib/datadog/ci/{test.rb → recorder.rb} +6 -5
  22. data/lib/datadog/ci/test_visibility/flush.rb +40 -0
  23. data/lib/datadog/ci/test_visibility/serializers/base.rb +161 -0
  24. data/lib/datadog/ci/test_visibility/serializers/factories/test_level.rb +30 -0
  25. data/lib/datadog/ci/test_visibility/serializers/span.rb +51 -0
  26. data/lib/datadog/ci/test_visibility/serializers/test_v1.rb +60 -0
  27. data/lib/datadog/ci/test_visibility/transport.rb +169 -0
  28. data/lib/datadog/ci/transport/gzip.rb +20 -0
  29. data/lib/datadog/ci/transport/http.rb +153 -0
  30. data/lib/datadog/ci/version.rb +2 -2
  31. data/sig/datadog/ci/configuration/components.rbs +2 -0
  32. data/sig/datadog/ci/configuration/settings.rbs +2 -0
  33. data/sig/datadog/ci/contrib/cucumber/configuration/settings.rbs +1 -1
  34. data/sig/datadog/ci/contrib/cucumber/integration.rbs +2 -1
  35. data/sig/datadog/ci/contrib/integration.rbs +44 -0
  36. data/sig/datadog/ci/contrib/minitest/configuration/settings.rbs +1 -1
  37. data/sig/datadog/ci/contrib/minitest/integration.rbs +4 -3
  38. data/sig/datadog/ci/contrib/rspec/configuration/settings.rbs +1 -1
  39. data/sig/datadog/ci/contrib/rspec/integration.rbs +3 -2
  40. data/sig/datadog/ci/contrib/settings.rbs +25 -0
  41. data/sig/datadog/ci/ext/settings.rbs +2 -0
  42. data/sig/datadog/ci/ext/transport.rbs +21 -0
  43. data/sig/datadog/ci/{test.rbs → recorder.rbs} +1 -1
  44. data/sig/datadog/ci/test_visibility/flush.rbs +17 -0
  45. data/sig/datadog/ci/test_visibility/serializers/base.rbs +73 -0
  46. data/sig/datadog/ci/test_visibility/serializers/factories/test_level.rbs +13 -0
  47. data/sig/datadog/ci/test_visibility/serializers/span.rbs +18 -0
  48. data/sig/datadog/ci/test_visibility/serializers/test_v1.rbs +23 -0
  49. data/sig/datadog/ci/test_visibility/transport.rbs +39 -0
  50. data/sig/datadog/ci/transport/gzip.rbs +9 -0
  51. data/sig/datadog/ci/transport/http.rbs +61 -0
  52. metadata +41 -7
  53. data/lib/datadog/ci/flush.rb +0 -38
  54. data/sig/datadog/ci/flush.rbs +0 -15
@@ -0,0 +1,33 @@
1
+ require "datadog/core/configuration/base"
2
+
3
+ module Datadog
4
+ module CI
5
+ module Contrib
6
+ # Common settings for all integrations
7
+ # @public_api
8
+ class Settings
9
+ include Core::Configuration::Base
10
+
11
+ option :enabled, default: true
12
+ option :service_name
13
+ option :operation_name
14
+
15
+ def configure(options = {})
16
+ self.class.options.each do |name, _value|
17
+ self[name] = options[name] if options.key?(name)
18
+ end
19
+
20
+ yield(self) if block_given?
21
+ end
22
+
23
+ def [](name)
24
+ respond_to?(name) ? send(name) : get_option(name)
25
+ end
26
+
27
+ def []=(name, value)
28
+ respond_to?("#{name}=") ? send("#{name}=", value) : set_option(name, value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -97,6 +97,13 @@ module Datadog
97
97
 
98
98
  raise "Failed to run git command #{cmd}: #{out}" unless status.success?
99
99
 
100
+ # Sometimes Encoding.default_external is somehow set to US-ASCII which breaks
101
+ # commit messages with UTF-8 characters like emojis
102
+ # We force output's encoding to be UTF-8 in this case
103
+ # This is safe to do as UTF-8 is compatible with US-ASCII
104
+ if Encoding.default_external == Encoding::US_ASCII
105
+ out = out.force_encoding(Encoding::UTF_8)
106
+ end
100
107
  out.strip! # There's always a "\n" at the end of the command output
101
108
 
102
109
  return nil if out.empty?
@@ -6,6 +6,8 @@ module Datadog
6
6
  # Defines constants for test tags
7
7
  module Settings
8
8
  ENV_MODE_ENABLED = "DD_TRACE_CI_ENABLED"
9
+ ENV_AGENTLESS_MODE_ENABLED = "DD_CIVISIBILITY_AGENTLESS_ENABLED"
10
+ ENV_AGENTLESS_URL = "DD_CIVISIBILITY_AGENTLESS_URL"
9
11
  end
10
12
  end
11
13
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Ext
6
+ module Transport
7
+ HEADER_DD_API_KEY = "DD-API-KEY"
8
+ HEADER_CONTENT_TYPE = "Content-Type"
9
+ HEADER_CONTENT_ENCODING = "Content-Encoding"
10
+
11
+ TEST_VISIBILITY_INTAKE_HOST_PREFIX = "citestcycle-intake"
12
+ TEST_VISIBILITY_INTAKE_PATH = "/api/v2/citestcycle"
13
+
14
+ CONTENT_TYPE_MESSAGEPACK = "application/msgpack"
15
+ CONTENT_ENCODING_GZIP = "gzip"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "datadog/tracing"
3
4
  require "datadog/tracing/contrib/analytics"
4
5
 
5
6
  require_relative "ext/app_types"
@@ -11,7 +12,7 @@ require "rbconfig"
11
12
  module Datadog
12
13
  module CI
13
14
  # Common behavior for CI tests
14
- module Test
15
+ module Recorder
15
16
  # Creates a new span for a CI test
16
17
  def self.trace(span_name, options = {})
17
18
  span_options = {
@@ -19,13 +20,13 @@ module Datadog
19
20
  }.merge(options[:span_options] || {})
20
21
 
21
22
  if block_given?
22
- Tracing.trace(span_name, **span_options) do |span, trace|
23
+ ::Datadog::Tracing.trace(span_name, **span_options) do |span, trace|
23
24
  set_tags!(trace, span, options)
24
25
  yield(span, trace)
25
26
  end
26
27
  else
27
- span = Tracing.trace(span_name, **span_options)
28
- trace = Tracing.active_trace
28
+ span = ::Datadog::Tracing.trace(span_name, **span_options)
29
+ trace = ::Datadog::Tracing.active_trace
29
30
  set_tags!(trace, span, options)
30
31
  span
31
32
  end
@@ -37,7 +38,7 @@ module Datadog
37
38
 
38
39
  # Set default tags
39
40
  trace.origin = Ext::Test::CONTEXT_ORIGIN if trace
40
- Datadog::Tracing::Contrib::Analytics.set_measured(span)
41
+ ::Datadog::Tracing::Contrib::Analytics.set_measured(span)
41
42
  span.set_tag(Ext::Test::TAG_SPAN_KIND, Ext::AppTypes::TYPE_TEST)
42
43
 
43
44
  # Set environment tags
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "datadog/tracing/metadata/ext"
4
+ require "datadog/tracing/flush"
5
+
6
+ module Datadog
7
+ module CI
8
+ module TestVisibility
9
+ module Flush
10
+ # Common behavior for CI flushing
11
+ module Tagging
12
+ # Decorate a trace with CI tags
13
+ def get_trace(trace_op)
14
+ trace = trace_op.flush!
15
+
16
+ # Origin tag is required on every span
17
+ trace.spans.each do |span|
18
+ span.set_tag(
19
+ Tracing::Metadata::Ext::Distributed::TAG_ORIGIN,
20
+ trace.origin
21
+ )
22
+ end
23
+
24
+ trace
25
+ end
26
+ end
27
+
28
+ # Consumes only completed traces (where all spans have finished)
29
+ class Finished < Tracing::Flush::Finished
30
+ prepend Tagging
31
+ end
32
+
33
+ # Performs partial trace flushing to avoid large traces residing in memory for too long
34
+ class Partial < Tracing::Flush::Partial
35
+ prepend Tagging
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module TestVisibility
6
+ module Serializers
7
+ class Base
8
+ MINIMUM_TIMESTAMP_NANO = 946684800000000000
9
+ MINIMUM_DURATION_NANO = 0
10
+ MAXIMUM_DURATION_NANO = 9223372036854775807
11
+
12
+ attr_reader :trace, :span
13
+
14
+ def initialize(trace, span)
15
+ @trace = trace
16
+ @span = span
17
+ end
18
+
19
+ def to_msgpack(packer = nil)
20
+ packer ||= MessagePack::Packer.new
21
+
22
+ packer.write_map_header(3)
23
+
24
+ write_field(packer, "type")
25
+ write_field(packer, "version")
26
+
27
+ packer.write("content")
28
+ packer.write_map_header(content_map_size)
29
+
30
+ content_fields.each do |field|
31
+ if field.is_a?(Hash)
32
+ field.each do |field_name, method|
33
+ write_field(packer, field_name, method)
34
+ end
35
+ else
36
+ write_field(packer, field)
37
+ end
38
+ end
39
+ end
40
+
41
+ # validates according to citestcycle json schema
42
+ def valid?
43
+ required_fields_present? && valid_start_time? && valid_duration?
44
+ end
45
+
46
+ def content_fields
47
+ []
48
+ end
49
+
50
+ def content_map_size
51
+ 0
52
+ end
53
+
54
+ def runtime_id
55
+ @trace.runtime_id
56
+ end
57
+
58
+ def trace_id
59
+ @trace.id
60
+ end
61
+
62
+ def span_id
63
+ @span.id
64
+ end
65
+
66
+ def parent_id
67
+ @span.parent_id
68
+ end
69
+
70
+ def type
71
+ end
72
+
73
+ def version
74
+ 1
75
+ end
76
+
77
+ def span_type
78
+ @span.type
79
+ end
80
+
81
+ def name
82
+ @span.name
83
+ end
84
+
85
+ def resource
86
+ @span.resource
87
+ end
88
+
89
+ def service
90
+ @span.service
91
+ end
92
+
93
+ def start
94
+ @start ||= time_nano(@span.start_time)
95
+ end
96
+
97
+ def duration
98
+ @duration ||= duration_nano(@span.duration)
99
+ end
100
+
101
+ def meta
102
+ @span.meta
103
+ end
104
+
105
+ def metrics
106
+ @span.metrics
107
+ end
108
+
109
+ def error
110
+ @span.status
111
+ end
112
+
113
+ def self.calculate_content_map_size(fields_list)
114
+ fields_list.reduce(0) do |size, field|
115
+ if field.is_a?(Hash)
116
+ size + field.size
117
+ else
118
+ size + 1
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def valid_start_time?
126
+ !start.nil? && start >= MINIMUM_TIMESTAMP_NANO
127
+ end
128
+
129
+ def valid_duration?
130
+ !duration.nil? && duration >= MINIMUM_DURATION_NANO && duration <= MAXIMUM_DURATION_NANO
131
+ end
132
+
133
+ def required_fields_present?
134
+ required_fields.all? { |field| !send(field).nil? }
135
+ end
136
+
137
+ def required_fields
138
+ []
139
+ end
140
+
141
+ def write_field(packer, field_name, method = nil)
142
+ method ||= field_name
143
+
144
+ packer.write(field_name)
145
+ packer.write(send(method))
146
+ end
147
+
148
+ # in nanoseconds since Epoch
149
+ def time_nano(time)
150
+ time.to_i * 1000000000 + time.nsec
151
+ end
152
+
153
+ # in nanoseconds
154
+ def duration_nano(duration)
155
+ (duration * 1e9).to_i
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_v1"
4
+ require_relative "../span"
5
+
6
+ module Datadog
7
+ module CI
8
+ module TestVisibility
9
+ module Serializers
10
+ module Factories
11
+ # This factory takes care of creating citestcycle serializers when test-level visibility is enabled
12
+ # NOTE: citestcycle is a protocol Datadog uses to submit test execution tracing information to CI visibility
13
+ # backend
14
+ module TestLevel
15
+ module_function
16
+
17
+ def serializer(trace, span)
18
+ case span.type
19
+ when Datadog::CI::Ext::AppTypes::TYPE_TEST
20
+ Serializers::TestV1.new(trace, span)
21
+ else
22
+ Serializers::Span.new(trace, span)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Datadog
6
+ module CI
7
+ module TestVisibility
8
+ module Serializers
9
+ class Span < Base
10
+ CONTENT_FIELDS = [
11
+ "trace_id", "span_id", "parent_id",
12
+ "name", "resource", "service",
13
+ "error", "start", "duration",
14
+ "meta", "metrics",
15
+ "type" => "span_type"
16
+ ].freeze
17
+
18
+ CONTENT_MAP_SIZE = calculate_content_map_size(CONTENT_FIELDS)
19
+
20
+ REQUIRED_FIELDS = [
21
+ "trace_id",
22
+ "span_id",
23
+ "error",
24
+ "name",
25
+ "resource",
26
+ "start",
27
+ "duration"
28
+ ].freeze
29
+
30
+ def content_fields
31
+ CONTENT_FIELDS
32
+ end
33
+
34
+ def content_map_size
35
+ CONTENT_MAP_SIZE
36
+ end
37
+
38
+ def type
39
+ "span"
40
+ end
41
+
42
+ private
43
+
44
+ def required_fields
45
+ REQUIRED_FIELDS
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../../ext/test"
5
+
6
+ module Datadog
7
+ module CI
8
+ module TestVisibility
9
+ module Serializers
10
+ class TestV1 < Base
11
+ CONTENT_FIELDS = [
12
+ "trace_id", "span_id",
13
+ "name", "resource", "service",
14
+ "error", "start", "duration",
15
+ "meta", "metrics",
16
+ "type" => "span_type"
17
+ ].freeze
18
+
19
+ CONTENT_MAP_SIZE = calculate_content_map_size(CONTENT_FIELDS)
20
+
21
+ REQUIRED_FIELDS = [
22
+ "trace_id",
23
+ "span_id",
24
+ "error",
25
+ "name",
26
+ "resource",
27
+ "start",
28
+ "duration"
29
+ ].freeze
30
+
31
+ def content_fields
32
+ CONTENT_FIELDS
33
+ end
34
+
35
+ def content_map_size
36
+ CONTENT_MAP_SIZE
37
+ end
38
+
39
+ def type
40
+ "test"
41
+ end
42
+
43
+ def name
44
+ "#{@span.get_tag(Ext::Test::TAG_FRAMEWORK)}.test"
45
+ end
46
+
47
+ def resource
48
+ "#{@span.get_tag(Ext::Test::TAG_SUITE)}.#{@span.get_tag(Ext::Test::TAG_NAME)}"
49
+ end
50
+
51
+ private
52
+
53
+ def required_fields
54
+ REQUIRED_FIELDS
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "msgpack"
4
+ require "uri"
5
+
6
+ require "datadog/core/encoding"
7
+ require "datadog/core/environment/identity"
8
+ require "datadog/core/chunker"
9
+
10
+ require_relative "serializers/factories/test_level"
11
+ require_relative "../ext/transport"
12
+ require_relative "../transport/http"
13
+
14
+ module Datadog
15
+ module CI
16
+ module TestVisibility
17
+ class Transport
18
+ # CI test cycle intake's limit is 5.1MB uncompressed
19
+ # We will use a bit more conservative value 5MB
20
+ DEFAULT_MAX_PAYLOAD_SIZE = 5 * 1024 * 1024
21
+
22
+ attr_reader :serializers_factory,
23
+ :api_key,
24
+ :max_payload_size,
25
+ :http,
26
+ :env
27
+
28
+ def initialize(
29
+ api_key:,
30
+ url:,
31
+ env: nil,
32
+ serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel,
33
+ max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE
34
+ )
35
+ @serializers_factory = serializers_factory
36
+ @api_key = api_key
37
+ @max_payload_size = max_payload_size
38
+ @env = env
39
+
40
+ uri = URI.parse(url)
41
+
42
+ raise "Invalid agentless mode URL: #{url}" if uri.host.nil?
43
+
44
+ @http = Datadog::CI::Transport::HTTP.new(
45
+ host: uri.host,
46
+ port: uri.port,
47
+ ssl: uri.scheme == "https" || uri.port == 443,
48
+ compress: true
49
+ )
50
+ end
51
+
52
+ def send_traces(traces)
53
+ return [] if traces.nil? || traces.empty?
54
+
55
+ Datadog.logger.debug { "Sending #{traces.count} traces..." }
56
+
57
+ encoded_events = encode_traces(traces)
58
+ if encoded_events.empty?
59
+ Datadog.logger.debug { "Empty encoded events list, skipping send" }
60
+ return []
61
+ end
62
+
63
+ responses = []
64
+ Datadog::Core::Chunker.chunk_by_size(encoded_events, max_payload_size).map do |chunk|
65
+ encoded_payload = pack_events(chunk)
66
+ Datadog.logger.debug do
67
+ "Send chunk of #{chunk.count} events; payload size #{encoded_payload.size}"
68
+ end
69
+
70
+ response = send_payload(encoded_payload)
71
+
72
+ Datadog.logger.debug do
73
+ "Received server response: #{response.inspect}"
74
+ end
75
+
76
+ responses << response
77
+ end
78
+
79
+ responses
80
+ end
81
+
82
+ private
83
+
84
+ def send_payload(encoded_payload)
85
+ http.request(
86
+ path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH,
87
+ payload: encoded_payload,
88
+ headers: {
89
+ Ext::Transport::HEADER_DD_API_KEY => api_key,
90
+ Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK
91
+ }
92
+ )
93
+ end
94
+
95
+ def encode_traces(traces)
96
+ traces.flat_map do |trace|
97
+ spans = trace.spans
98
+ # TODO: remove condition when 1.0 is released
99
+ if spans.respond_to?(:filter_map)
100
+ spans.filter_map { |span| encode_span(trace, span) }
101
+ else
102
+ trace.spans.map { |span| encode_span(trace, span) }.reject(&:nil?)
103
+ end
104
+ end
105
+ end
106
+
107
+ def encode_span(trace, span)
108
+ serializer = serializers_factory.serializer(trace, span)
109
+
110
+ if serializer.valid?
111
+ encoded = encoder.encode(serializer)
112
+
113
+ if encoded.size > max_payload_size
114
+ # This single event is too large, we can't flush it
115
+ Datadog.logger.debug { "Dropping test event. Payload too large: '#{span.inspect}'" }
116
+ Datadog.logger.debug { encoded }
117
+
118
+ return nil
119
+ end
120
+
121
+ encoded
122
+ else
123
+ Datadog.logger.debug { "Invalid span skipped: #{span}" }
124
+ nil
125
+ end
126
+ end
127
+
128
+ def encoder
129
+ Datadog::Core::Encoding::MsgpackEncoder
130
+ end
131
+
132
+ def pack_events(encoded_events)
133
+ packer = MessagePack::Packer.new
134
+
135
+ packer.write_map_header(3) # Set header with how many elements in the map
136
+
137
+ packer.write("version")
138
+ packer.write(1)
139
+
140
+ packer.write("metadata")
141
+ packer.write_map_header(1)
142
+
143
+ packer.write("*")
144
+ metadata_fields_count = env ? 4 : 3
145
+ packer.write_map_header(metadata_fields_count)
146
+
147
+ if env
148
+ packer.write("env")
149
+ packer.write(env)
150
+ end
151
+
152
+ packer.write("runtime-id")
153
+ packer.write(Datadog::Core::Environment::Identity.id)
154
+
155
+ packer.write("language")
156
+ packer.write(Datadog::Core::Environment::Identity.lang)
157
+
158
+ packer.write("library_version")
159
+ packer.write(Datadog::CI::VERSION::STRING)
160
+
161
+ packer.write("events")
162
+ packer.write_array_header(encoded_events.size)
163
+
164
+ (packer.buffer.to_a + encoded_events).join
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "stringio"
5
+
6
+ module Datadog
7
+ module CI
8
+ module Transport
9
+ module Gzip
10
+ module_function
11
+
12
+ def compress(input)
13
+ gzip_writer = Zlib::GzipWriter.new(StringIO.new)
14
+ gzip_writer << input
15
+ gzip_writer.close.string
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end