1
0
mirror of https://github.com/bitwarden/server synced 2025-12-24 04:03:25 +00:00

Add Datadog integration (#6289)

* Event integration updates and cleanups

* Add Datadog integration

* Update README to include link to Datadog PR

* Move doc update into the Datadog PR; Fix empty message on ArgumentException

* Adjust exception message

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

* Removed unnecessary nullable enable; Moved Docs link to PR into this PR

* Remove unnecessary nullable enable calls

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
Brant DeBow
2025-09-08 12:39:59 -04:00
committed by GitHub
parent 39ad020418
commit 747e212b1b
14 changed files with 346 additions and 7 deletions

View File

@@ -1,6 +1,4 @@
#nullable enable
using System.Text.Json;
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
@@ -36,6 +34,10 @@ public class OrganizationIntegrationConfigurationRequestModel
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
case IntegrationType.Datadog:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
default:
return false;

View File

@@ -4,8 +4,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
#nullable enable
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
public class OrganizationIntegrationRequestModel : IValidatableObject
@@ -60,6 +58,14 @@ public class OrganizationIntegrationRequestModel : IValidatableObject
new[] { nameof(Configuration) });
}
break;
case IntegrationType.Datadog:
if (!IsIntegrationValid<DatadogIntegration>())
{
yield return new ValidationResult(
"Datadog integrations must include valid configuration.",
new[] { nameof(Configuration) });
}
break;
default:
yield return new ValidationResult(
$"Integration type '{Type}' is not recognized.",

View File

@@ -6,7 +6,8 @@ public enum IntegrationType : int
Scim = 2,
Slack = 3,
Webhook = 4,
Hec = 5
Hec = 5,
Datadog = 6
}
public static class IntegrationTypeExtensions
@@ -21,6 +22,8 @@ public static class IntegrationTypeExtensions
return "webhook";
case IntegrationType.Hec:
return "hec";
case IntegrationType.Datadog:
return "datadog";
default:
throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}");
}

View File

@@ -0,0 +1,3 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record DatadogIntegration(string ApiKey, Uri Uri);

View File

@@ -0,0 +1,3 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record DatadogIntegrationConfigurationDetails(string ApiKey, Uri Uri);

View File

@@ -0,0 +1,38 @@
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class DatadogListenerConfiguration(GlobalSettings globalSettings)
: ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration
{
public IntegrationType IntegrationType
{
get => IntegrationType.Datadog;
}
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.DatadogEventsQueueName;
}
public string IntegrationQueueName
{
get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationQueueName;
}
public string IntegrationRetryQueueName
{
get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationRetryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.DatadogEventSubscriptionName;
}
public string IntegrationSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.DatadogIntegrationSubscriptionName;
}
}

View File

@@ -0,0 +1,25 @@
using System.Text;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
namespace Bit.Core.Services;
public class DatadogIntegrationHandler(
IHttpClientFactory httpClientFactory,
TimeProvider timeProvider)
: IntegrationHandlerBase<DatadogIntegrationConfigurationDetails>
{
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
public const string HttpClientName = "DatadogIntegrationHandlerHttpClient";
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri);
request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
request.Headers.Add("DD-API-KEY", message.Configuration.ApiKey);
var response = await _httpClient.SendAsync(request);
return ResultFromHttpResponse(response, message, timeProvider);
}
}

View File

@@ -323,7 +323,8 @@ A hosted service (`IntegrationConfigurationDetailsCacheService`) runs in the bac
# Building a new integration
These are all the pieces required in the process of building out a new integration. For
clarity in naming, these assume a new integration called "Example".
clarity in naming, these assume a new integration called "Example". To see a complete example
in context, view [the PR for adding the Datadog integration](https://github.com/bitwarden/server/pull/6289).
## IntegrationType

View File

@@ -304,6 +304,8 @@ public class GlobalSettings : IGlobalSettings
public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription";
public virtual string HecEventSubscriptionName { get; set; } = "events-hec-subscription";
public virtual string HecIntegrationSubscriptionName { get; set; } = "integration-hec-subscription";
public virtual string DatadogEventSubscriptionName { get; set; } = "events-datadog-subscription";
public virtual string DatadogIntegrationSubscriptionName { get; set; } = "integration-datadog-subscription";
public string ConnectionString
{
@@ -345,6 +347,9 @@ public class GlobalSettings : IGlobalSettings
public virtual string HecEventsQueueName { get; set; } = "events-hec-queue";
public virtual string HecIntegrationQueueName { get; set; } = "integration-hec-queue";
public virtual string HecIntegrationRetryQueueName { get; set; } = "integration-hec-retry-queue";
public virtual string DatadogEventsQueueName { get; set; } = "events-datadog-queue";
public virtual string DatadogIntegrationQueueName { get; set; } = "integration-datadog-queue";
public virtual string DatadogIntegrationRetryQueueName { get; set; } = "integration-datadog-retry-queue";
public string HostName
{

View File

@@ -881,15 +881,18 @@ public static class ServiceCollectionExtensions
services.AddSlackService(globalSettings);
services.TryAddSingleton(TimeProvider.System);
services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
services.AddHttpClient(DatadogIntegrationHandler.HttpClientName);
// Add integration handlers
services.TryAddSingleton<IIntegrationHandler<SlackIntegrationConfigurationDetails>, SlackIntegrationHandler>();
services.TryAddSingleton<IIntegrationHandler<WebhookIntegrationConfigurationDetails>, WebhookIntegrationHandler>();
services.TryAddSingleton<IIntegrationHandler<DatadogIntegrationConfigurationDetails>, DatadogIntegrationHandler>();
var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings);
var slackConfiguration = new SlackListenerConfiguration(globalSettings);
var webhookConfiguration = new WebhookListenerConfiguration(globalSettings);
var hecConfiguration = new HecListenerConfiguration(globalSettings);
var datadogConfiguration = new DatadogListenerConfiguration(globalSettings);
if (IsRabbitMqEnabled(globalSettings))
{
@@ -906,6 +909,7 @@ public static class ServiceCollectionExtensions
services.AddRabbitMqIntegration<SlackIntegrationConfigurationDetails, SlackListenerConfiguration>(slackConfiguration);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, WebhookListenerConfiguration>(webhookConfiguration);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, HecListenerConfiguration>(hecConfiguration);
services.AddRabbitMqIntegration<DatadogIntegrationConfigurationDetails, DatadogListenerConfiguration>(datadogConfiguration);
}
if (IsAzureServiceBusEnabled(globalSettings))
@@ -923,6 +927,7 @@ public static class ServiceCollectionExtensions
services.AddAzureServiceBusIntegration<SlackIntegrationConfigurationDetails, SlackListenerConfiguration>(slackConfiguration);
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, WebhookListenerConfiguration>(webhookConfiguration);
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, HecListenerConfiguration>(hecConfiguration);
services.AddAzureServiceBusIntegration<DatadogIntegrationConfigurationDetails, DatadogListenerConfiguration>(datadogConfiguration);
}
return services;