gitlab-ce/app/models/integrations/datadog.rb

297 lines
11 KiB
Ruby

# 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/"
CI_LOGS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/continuous_integration/pipelines/gitlab/?tab=gitlabcom#collect-job-logs"
CI_VISIBILITY_PRICING = "https://www.#{DEFAULT_DOMAIN}/pricing/?product=ci-pipeline-visibility#products"
SITES_DOCS = "https://docs.#{DEFAULT_DOMAIN}/getting_started/site/"
SUPPORTED_EVENTS = %w[
pipeline build archive_trace push merge_request note tag_push subgroup project
].freeze
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x
# The config is divided in two sections:
# - General account configuration, which allows setting up a Datadog site and API key
# - CI Visibility configuration, which is specific to job & pipeline events
def sections
[
{
type: SECTION_TYPE_CONNECTION,
title: s_('DatadogIntegration|Datadog account'),
description: help
},
{
type: SECTION_TYPE_CONFIGURATION,
title: s_('DatadogIntegration|CI Visibility'),
description: s_('DatadogIntegration|Additionally, enable CI Visibility to send pipeline information to Datadog to monitor for job failures and troubleshoot performance issues.')
}
]
end
# General account configuration
field :datadog_site,
exposes_secrets: true,
section: SECTION_TYPE_CONNECTION,
placeholder: DEFAULT_DOMAIN,
help: -> do
docs_link = ActionController::Base.helpers.link_to('', SITES_DOCS, target: '_blank', rel: 'noopener noreferrer')
tag_pair_docs_link = tag_pair(docs_link, :link_start, :link_end)
safe_format(s_('DatadogIntegration|Datadog site to send data to. Learn more about Datadog sites in the %{link_start}documentation%{link_end}.'), tag_pair_docs_link)
end
field :api_url,
exposes_secrets: true,
section: SECTION_TYPE_CONNECTION,
title: -> { s_('DatadogIntegration|API URL') },
help: -> { s_('DatadogIntegration|Full URL of your Datadog site. Only required if you do not use a standard Datadog site.') }
field :api_key,
type: :password,
section: SECTION_TYPE_CONNECTION,
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
# CI Visibility section
field :datadog_ci_visibility,
type: :checkbox,
section: SECTION_TYPE_CONFIGURATION,
title: -> { s_('DatadogIntegration|Enabled') },
checkbox_label: -> { s_('DatadogIntegration|Enable CI Visibility') },
description: -> { _('Enable CI Visibility') },
help: -> do
docs_link = ActionController::Base.helpers.link_to('', CI_VISIBILITY_PRICING, target: '_blank', rel: 'noopener noreferrer')
tag_pair_docs_link = tag_pair(docs_link, :link_start, :link_end)
safe_format(s_('DatadogIntegration|When enabled, pipelines and jobs are collected, and Datadog will display pipeline execution traces. Note that CI Visibility is priced per committers, see our %{link_start}pricing page%{link_end}.'), tag_pair_docs_link)
end
field :archive_trace_events,
storage: :attribute,
type: :checkbox,
section: SECTION_TYPE_CONFIGURATION,
title: -> { _('Logs') },
checkbox_label: -> { _('Enable Pipeline Job logs collection') },
help: -> do
docs_link = ActionController::Base.helpers.link_to('', CI_LOGS_DOCS, target: '_blank', rel: 'noopener noreferrer')
tag_pair_docs_link = tag_pair(docs_link, :link_start, :link_end)
safe_format(s_('DatadogIntegration|When enabled, pipeline job logs are collected by Datadog and displayed along with pipeline execution traces. This requires CI Visibility to be enabled. Note that pipeline job logs are priced like regular Datadog logs. Learn more %{link_start}here%{link_end}.'), tag_pair_docs_link)
end
field :datadog_service,
title: -> { s_('DatadogIntegration|Service') },
section: SECTION_TYPE_CONFIGURATION,
placeholder: 'gitlab-ci',
help: -> { s_('DatadogIntegration|Tag all pipeline data from this GitLab instance in Datadog. Can be used when managing several self-managed deployments.') }
field :datadog_env,
title: -> { s_('DatadogIntegration|Environment') },
section: SECTION_TYPE_CONFIGURATION,
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 pipeline 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,
section: SECTION_TYPE_CONFIGURATION,
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 for pipeline data 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? }
validates :datadog_ci_visibility, inclusion: [true, false]
validate :datadog_tags_are_valid
validate :logs_requires_ci_vis
end
def initialize_properties
super
self.datadog_site ||= DEFAULT_DOMAIN
# Previous versions of the integration don't have the datadog_ci_visibility boolean stored in the configuration.
# Since the previous default was for this to be enabled, we want this attribute to be initialized to true
# if the integration was previously active. Otherwise, it should default to false.
if datadog_ci_visibility.nil?
self.datadog_ci_visibility = active
end
end
attribute :pipeline_events, default: false
attribute :job_events, default: false
before_save :update_pipeline_events
def update_pipeline_events
# pipeline and job events are opt-in, controlled by a single datadog_ci_visibility checkbox
unless datadog_ci_visibility.nil?
self.job_events = datadog_ci_visibility
self.pipeline_events = datadog_ci_visibility
end
end
def self.supported_events
SUPPORTED_EVENTS
end
def self.default_test_event
'push'
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|Connect your projects to Datadog and trace your GitLab pipelines.')
end
def self.help
build_help_page_url(
'integration/datadog.md',
s_('DatadogIntegration|Connect your GitLab projects to your Datadog account to synchronize repository metadata and enrich telemetry on your Datadog account.'),
link_text: _('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
def logs_requires_ci_vis
if archive_trace_events && !datadog_ci_visibility
errors.add(:archive_trace_events, s_("DatadogIntegration|requires CI Visibility to be enabled"))
end
end
end
end