datadog-ci 0.1.1 → 0.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 (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