Skip to content

Elasticsearch

Elasticsearch is a distributed, RESTful search and analytics engine capable of addressing a growing number of use cases. As the heart of the Elastic Stack, it centrally stores data for lightning fast search, fine‑tuned relevancy, and powerful analytics that scale with ease.

Add the following dependency to your project file:

NuGet
1
dotnet add package Testcontainers.Elasticsearch

You can start an Elasticsearch container instance from any .NET application. Here, we create different container instances and pass them to the base test class. This allows us to test different configurations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[UsedImplicitly]
public sealed class ElasticsearchDefaultConfiguration : ElasticsearchContainerTest
{
    public ElasticsearchDefaultConfiguration()
        : base(new ElasticsearchBuilder(TestSession.GetImageFromDockerfile()).Build())
    {
    }
}

[UsedImplicitly]
public sealed class ElasticsearchAuthConfiguration : ElasticsearchContainerTest
{
    public ElasticsearchAuthConfiguration()
        : base(new ElasticsearchBuilder(TestSession.GetImageFromDockerfile()).WithPassword("some-password").Build())
    {
    }
}

This example uses xUnit.net's IAsyncLifetime interface to manage the lifecycle of the container. The container is started in the InitializeAsync method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the DisposeAsync method.

 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
public async ValueTask InitializeAsync()
{
    await _elasticsearchContainer.StartAsync()
        .ConfigureAwait(false);
}

public async ValueTask DisposeAsync()
{
    await DisposeAsyncCore()
        .ConfigureAwait(false);

    GC.SuppressFinalize(this);
}

[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task PingReturnsValidResponse()
{
    // Given
    using var caCertificate = await _elasticsearchContainer.GetCertificateAsync(TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    var connectionString = new Uri(_elasticsearchContainer.GetConnectionString());

    var clientSettings = new ElasticsearchClientSettings(connectionString);
    clientSettings.ServerCertificateValidationCallback(CertificateValidations.AuthorityIsRoot(caCertificate));

    var client = new ElasticsearchClient(clientSettings);

    // When
    var response = await client.PingAsync(TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    // Then
    Assert.True(response.IsValidResponse);
    Assert.Equal(_elasticsearchContainer.GetConnectionString(), _elasticsearchContainer.GetConnectionString(ConnectionMode.Host));
}

The test example uses the following NuGet dependencies:

1
2
3
4
5
6
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="xunit.runner.visualstudio"/>
<PackageReference Include="xunit.v3"/>
<PackageReference Include="Elastic.Clients.Elasticsearch"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol"/>

To execute the tests, use the command dotnet test from a terminal.

Tip

For the complete source code of this example and additional information, please refer to our test projects.

OpenTelemetry (OTLP)

Elasticsearch 9.5 and later accept OTLP over HTTP. ElasticsearchContainer.GetOtlpEndpoint() returns the base endpoint the exporter sends the telemetry data to. If the endpoint is set via OTEL_EXPORTER_OTLP_ENDPOINT, the exporter appends the signal path, such as v1/metrics. If the endpoint is set via OtlpExporterOptions.Endpoint, the exporter uses it as is, and the signal path must be appended manually. In contrast to the connection string, the endpoint does not contain the credentials. Clients must send them in the Authorization header:

 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
[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task OtlpExportSpanIsIngested()
{
    // Given
    using var httpClientFactory = await ElasticsearchHttpClientFactory.CreateAsync(_elasticsearchContainer, TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    using var httpClient = httpClientFactory.CreateHttpClient();

    var spansJson = string.Empty;

    var serviceName = Guid.NewGuid().ToString("D");

    var spanName = Guid.NewGuid().ToString("D");

    var resourceBuilder = ResourceBuilder.CreateDefault().AddService(serviceName);

    var otlpExporterConfiguration = new Dictionary<string, string>
    {
        { "OTEL_EXPORTER_OTLP_ENDPOINT", _elasticsearchContainer.GetOtlpEndpoint() },
        { "OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf" },
    };

    var configuration = new ConfigurationBuilder()
        .AddInMemoryCollection(otlpExporterConfiguration)
        .Build();

    var tracerProviderBuilder = Sdk
        .CreateTracerProviderBuilder()
        .SetResourceBuilder(resourceBuilder)
        .AddSource(serviceName)
        .AddOtlpExporter(options => options.HttpClientFactory = () => httpClient)
        .ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration));

    // When
    using (var _ = tracerProviderBuilder.Build())
    {
        using var activitySource = new ActivitySource(serviceName);
        using var activity = activitySource.StartActivity(spanName);
        activity.SetTag("test.key", "test-value");
    }

    var spanIsIndexed = async () =>
    {
        using var httpResponseMessage = await httpClient.PostAsync("/traces-*/_refresh", null, TestContext.Current.CancellationToken)
            .ConfigureAwait(false);

        spansJson = await httpClient.GetStringAsync("/traces-*/_search", TestContext.Current.CancellationToken)
            .ConfigureAwait(false);

        return spansJson.Contains(spanName);
    };

    await WaitStrategy.WaitUntilAsync(spanIsIndexed, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(1), ct: TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    // Then
    Assert.Contains(serviceName, spansJson);
    Assert.Contains(spanName, spansJson);
}

A note to developers

The Testcontainers module creates a container that listens to requests over HTTPS. Elasticsearch generates a self-signed certificate authority (CA) during the startup that signs the HTTP certificate. ElasticsearchContainer.GetCertificateAsync() reads this certificate authority (CA) from the container. Configure the client to trust it, otherwise .NET will reject the certificate coming from the container.

Besides the Elasticsearch client, any other client can be configured to trust the certificate authority (CA) too. The example below uses the CertificateValidations.AuthorityIsRoot(X509Certificate) helper from Elastic.Transport. Without it, build an X509Chain with X509ChainTrustMode.CustomRootTrust and add the certificate authority (CA) to ChainPolicy.CustomTrustStore:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task ClusterHealthReturnsValidResponse()
{
    // Given
    using var httpClientFactory = await ElasticsearchHttpClientFactory.CreateAsync(_elasticsearchContainer, TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    using var httpClient = httpClientFactory.CreateHttpClient();

    // When
    using var httpResponseMessage = await httpClient.GetAsync("/_cluster/health", TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    // Then
    Assert.Equal(HttpStatusCode.OK, httpResponseMessage.StatusCode);
}