1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
|
# frozen_string_literal: true
module Integrations
class Datadog < Integration
include HasWebHook
DEFAULT_DOMAIN = 'datadoghq.com'
URL_TEMPLATE = 'https://webhook-intake.%{datadog_domain}/api/v2/webhook'
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
pipeline build archive_trace
].freeze
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x
field :datadog_site,
exposes_secrets: true,
placeholder: DEFAULT_DOMAIN,
help: -> do
ERB::Util.html_escape(
s_('DatadogIntegration|Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
) % {
codeOpen: '<code>'.html_safe,
codeClose: '</code>'.html_safe
}
end
field :api_url,
exposes_secrets: true,
title: -> { s_('DatadogIntegration|API URL') },
help: -> { s_('DatadogIntegration|Full URL of your Datadog site.') }
field :api_key,
type: :password,
title: -> { _('API key') },
non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key') },
help: -> do
docs_link = ActionController::Base.helpers.link_to('', URL_API_KEYS_DOCS, target: '_blank', rel: 'noopener noreferrer')
tag_pair_docs_link = tag_pair(docs_link, :link_start, :link_end)
safe_format(s_('DatadogIntegration|%{link_start}API key%{link_end} used for authentication with Datadog.'), tag_pair_docs_link)
end,
required: true
field :archive_trace_events,
storage: :attribute,
type: :checkbox,
title: -> { _('Logs') },
checkbox_label: -> { _('Enable logs collection') },
help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') }
field :datadog_service,
title: -> { s_('DatadogIntegration|Service') },
placeholder: 'gitlab-ci',
help: -> { s_('DatadogIntegration|GitLab instance to tag all data from in Datadog. Can be used when managing several self-managed deployments.') }
field :datadog_env,
title: -> { s_('DatadogIntegration|Environment') },
placeholder: 'ci',
description: -> { _('For self-managed deployments, `env` tag for all the data sent to Datadog.') },
help: -> do
ERB::Util.html_escape(
s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
) % {
codeOpen: '<code>'.html_safe,
codeClose: '</code>'.html_safe,
linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
linkClose: '</a>'.html_safe
}
end
field :datadog_tags,
type: :textarea,
title: -> { s_('DatadogIntegration|Tags') },
placeholder: "tag:value\nanother_tag:value",
description: -> { _('Custom tags in Datadog. Specify one tag per line in the format `key:value\nkey2:value2`.') },
help: -> do
ERB::Util.html_escape(
s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
) % {
codeOpen: '<code>'.html_safe,
codeClose: '</code>'.html_safe,
linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
linkClose: '</a>'.html_safe
}
end
before_validation :strip_properties
with_options if: :activated? do
validates :api_key, presence: true, format: { with: /\A\w+\z/ }
validates :datadog_site, format: { with: %r{\A\w+([-.]\w+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?\z}, allow_blank: true }
validates :api_url, public_url: { allow_blank: true }
validates :datadog_site, presence: true, unless: ->(obj) { obj.api_url.present? }
validates :api_url, presence: true, unless: ->(obj) { obj.datadog_site.present? }
validate :datadog_tags_are_valid
end
def initialize_properties
super
self.datadog_site ||= DEFAULT_DOMAIN
end
def self.supported_events
SUPPORTED_EVENTS
end
def self.default_test_event
'pipeline'
end
def configurable_events
[] # do not allow to opt out of required hooks
# archive_trace is opt-in but we handle it with a more detailed field below
end
def self.title
'Datadog'
end
def self.description
s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.')
end
def self.help
build_help_page_url(
'integration/datadog.md',
s_('DatadogIntegration|Send CI/CD pipeline information to Datadog to monitor for job failures and troubleshoot performance issues.'),
_('How do I set up this integration?')
)
end
def self.to_param
'datadog'
end
override :hook_url
def hook_url
url = api_url.presence || sprintf(URL_TEMPLATE, datadog_domain: datadog_domain)
url = URI.parse(url)
query = {
"dd-api-key" => 'THIS_VALUE_WILL_BE_REPLACED',
service: datadog_service.presence,
env: datadog_env.presence,
tags: datadog_tags_query_param.presence
}.compact
url.query = query.to_query
url.to_s.gsub('THIS_VALUE_WILL_BE_REPLACED', '{api_key}')
end
def url_variables
{ 'api_key' => api_key }
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
object_kind = data[:object_kind]
object_kind = 'job' if object_kind == 'build'
data = hook_data(data, object_kind)
execute_web_hook!(data, "#{object_kind} hook")
end
def test(data)
result = execute(data)
{
success: (200..299).cover?(result.payload[:http_status]),
result: result.message
}
end
private
def datadog_domain
# Transparently ignore "app" prefix from datadog_site as the official docs table in
# https://docs.datadoghq.com/getting_started/site/ is confusing for internal URLs.
# US3 needs to keep a prefix but other datacenters cannot have the listed "app" prefix
datadog_site.delete_prefix("app.")
end
def hook_data(data, object_kind)
if object_kind == 'pipeline' && data.respond_to?(:with_retried_builds)
return data.with_retried_builds
end
data
end
def strip_properties
datadog_service.strip! if datadog_service && !datadog_service.frozen?
datadog_env.strip! if datadog_env && !datadog_env.frozen?
datadog_tags.strip! if datadog_tags && !datadog_tags.frozen?
end
def datadog_tags_are_valid
return unless datadog_tags
unless datadog_tags.split("\n").select(&:present?).all? { _1 =~ TAG_KEY_VALUE_RE }
errors.add(:datadog_tags, s_("DatadogIntegration|have an invalid format"))
end
end
def datadog_tags_query_param
return unless datadog_tags
datadog_tags.split("\n").filter_map do |tag|
tag.strip!
next if tag.blank?
if tag.include?(',')
"\"#{tag}\""
else
tag
end
end.join(',')
end
end
end
|