All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.camunda.zeebe.exporter.ElasticsearchClient Maven / Gradle / Ivy

/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.zeebe.exporter;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.camunda.zeebe.exporter.dto.BulkIndexAction;
import io.camunda.zeebe.exporter.dto.BulkIndexResponse;
import io.camunda.zeebe.exporter.dto.BulkIndexResponse.Error;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyRequest;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyRequest.Actions;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyRequest.Delete;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyRequest.DeleteAction;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyRequest.Phases;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyRequest.Policy;
import io.camunda.zeebe.exporter.dto.PutIndexLifecycleManagementPolicyResponse;
import io.camunda.zeebe.exporter.dto.PutIndexSettingsRequest;
import io.camunda.zeebe.exporter.dto.PutIndexSettingsRequest.Index;
import io.camunda.zeebe.exporter.dto.PutIndexSettingsRequest.Lifecycle;
import io.camunda.zeebe.exporter.dto.PutIndexSettingsResponse;
import io.camunda.zeebe.exporter.dto.PutIndexTemplateResponse;
import io.camunda.zeebe.exporter.dto.Template;
import io.camunda.zeebe.protocol.record.Record;
import io.camunda.zeebe.protocol.record.ValueType;
import io.camunda.zeebe.util.VersionUtil;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer.Sample;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.http.entity.EntityTemplate;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RestClient;

class ElasticsearchClient implements AutoCloseable {
  private static final ObjectMapper MAPPER = new ObjectMapper();

  private final RestClient client;
  private final ElasticsearchExporterConfiguration configuration;
  private final TemplateReader templateReader;
  private final RecordIndexRouter indexRouter;
  private final BulkIndexRequest bulkIndexRequest;

  private final ElasticsearchMetrics metrics;

  /**
   * Sample to measure the flush latency of the current bulk request.
   *
   * 

Time of how long an export bulk request is open and collects new records before flushing, * meaning latency until the next flush is done. */ private Sample flushLatencyMeasurement; ElasticsearchClient( final ElasticsearchExporterConfiguration configuration, final MeterRegistry meterRegistry) { this( configuration, new BulkIndexRequest(), RestClientFactory.of(configuration), new RecordIndexRouter(configuration.index), new TemplateReader(configuration), new ElasticsearchMetrics(meterRegistry)); } ElasticsearchClient( final ElasticsearchExporterConfiguration configuration, final MeterRegistry meterRegistry, final RestClient restClient) { this( configuration, new BulkIndexRequest(), restClient, new RecordIndexRouter(configuration.index), new TemplateReader(configuration), new ElasticsearchMetrics(meterRegistry)); } ElasticsearchClient( final ElasticsearchExporterConfiguration configuration, final BulkIndexRequest bulkIndexRequest, final RestClient client, final RecordIndexRouter indexRouter, final TemplateReader templateReader, final ElasticsearchMetrics metrics) { this.configuration = configuration; this.bulkIndexRequest = bulkIndexRequest; this.client = client; this.indexRouter = indexRouter; this.templateReader = templateReader; this.metrics = metrics; } @Override public void close() throws IOException { client.close(); } /** * Indexes a record to the batch of records that will be sent to Elasticsearch * * @param record the record that will be the source of the document * @param recordSequence the sequence number of the record * @return true if the record was appended to the batch, false if the record is already indexed in * the batch because only one copy of the record is allowed in the batch */ public boolean index(final Record record, final RecordSequence recordSequence) { if (bulkIndexRequest.isEmpty()) { flushLatencyMeasurement = metrics.startFlushLatencyMeasurement(); } final BulkIndexAction action = new BulkIndexAction( indexRouter.indexFor(record), indexRouter.idFor(record), indexRouter.routingFor(record)); return bulkIndexRequest.index(action, record, recordSequence); } /** * Flushes the bulk request to Elastic, unless it's currently empty. * * @throws ElasticsearchExporterException if not all items of the bulk were flushed successfully */ public void flush() { if (bulkIndexRequest.isEmpty()) { return; } metrics.recordBulkSize(bulkIndexRequest.size()); metrics.recordBulkMemorySize(bulkIndexRequest.memoryUsageBytes()); metrics.measureFlushDuration( () -> { try { exportBulk(); metrics.stopFlushLatencyMeasurement(flushLatencyMeasurement); bulkIndexRequest.clear(); } catch (final ElasticsearchExporterException e) { metrics.recordFailedFlush(); throw e; } }); } /** * Returns whether the exporter should call {@link #flush()} or not. * * @return true if {@link #flush()} should be called, false otherwise */ public boolean shouldFlush() { return bulkIndexRequest.memoryUsageBytes() >= configuration.bulk.memoryLimit || bulkIndexRequest.size() >= configuration.bulk.size; } /** * Creates an index template for the given value type, read from the resources. * * @return true if request was acknowledged */ public boolean putIndexTemplate(final ValueType valueType) { return putIndexTemplate(valueType, VersionUtil.getVersionLowerCase()); } public boolean putIndexTemplate(final ValueType valueType, final String version) { final String templateName = indexRouter.indexPrefixForValueType(valueType, version); final Template template = templateReader.readIndexTemplate( valueType, indexRouter.searchPatternForValueType(valueType, version), indexRouter.aliasNameForValueType(valueType)); return putIndexTemplate(templateName, template); } /** * Creates or updates the component template on the target Elasticsearch. The template is read * from {@link TemplateReader#readComponentTemplate()}. */ public boolean putComponentTemplate() { final Template template = templateReader.readComponentTemplate(); return putComponentTemplate(template); } private void exportBulk() { final BulkIndexResponse response; try { final var request = new Request("POST", "/_bulk"); final var body = new EntityTemplate(bulkIndexRequest); body.setContentType("application/x-ndjson"); request.setEntity(body); response = sendRequest(request, BulkIndexResponse.class); } catch (final IOException e) { throw new ElasticsearchExporterException("Failed to flush bulk", e); } if (response.errors()) { throwCollectedBulkError(response); } } private void throwCollectedBulkError(final BulkIndexResponse bulkResponse) { final var collectedErrors = new ArrayList(); bulkResponse.items().stream() .flatMap(item -> Optional.ofNullable(item.index()).stream()) .flatMap(index -> Optional.ofNullable(index.error()).stream()) .collect(Collectors.groupingBy(Error::type)) .forEach( (errorType, errors) -> collectedErrors.add( String.format( "Failed to flush %d item(s) of bulk request [type: %s, reason: %s]", errors.size(), errorType, errors.get(0).reason()))); throw new ElasticsearchExporterException("Failed to flush bulk request: " + collectedErrors); } private boolean putIndexTemplate(final String templateName, final Template template) { try { final var request = new Request("PUT", "/_index_template/" + templateName); request.setJsonEntity(MAPPER.writeValueAsString(template)); final var response = sendRequest(request, PutIndexTemplateResponse.class); return response.acknowledged(); } catch (final IOException e) { throw new ElasticsearchExporterException("Failed to put index template", e); } } private boolean putComponentTemplate(final Template template) { try { final var request = new Request("PUT", "/_component_template/" + configuration.index.prefix); request.setJsonEntity(MAPPER.writeValueAsString(template)); final var response = sendRequest(request, PutIndexTemplateResponse.class); return response.acknowledged(); } catch (final IOException e) { throw new ElasticsearchExporterException("Failed to put component template", e); } } public boolean putIndexLifecycleManagementPolicy() { try { final var request = new Request("PUT", "/_ilm/policy/" + configuration.retention.getPolicyName()); final var requestEntity = buildPutIndexLifecycleManagementPolicyRequest(configuration.retention.getMinimumAge()); request.setJsonEntity(MAPPER.writeValueAsString(requestEntity)); final var response = sendRequest(request, PutIndexLifecycleManagementPolicyResponse.class); return response.acknowledged(); } catch (final IOException e) { throw new ElasticsearchExporterException( "Failed to put index lifecycle management policy", e); } } public boolean bulkPutIndexLifecycleSettings(final String policyName) { try { final var request = new Request( "PUT", "/" + configuration.index.prefix + "*/_settings?allow_no_indices=true"); final var requestEntity = new PutIndexSettingsRequest(new Index(new Lifecycle(policyName))); request.setJsonEntity(MAPPER.writeValueAsString(requestEntity)); final var response = sendRequest(request, PutIndexSettingsResponse.class); return response.acknowledged(); } catch (final IOException e) { throw new ElasticsearchExporterException("Failed to update indices lifecycle settings", e); } } static PutIndexLifecycleManagementPolicyRequest buildPutIndexLifecycleManagementPolicyRequest( final String minimumAge) { return new PutIndexLifecycleManagementPolicyRequest( new Policy(new Phases(new Delete(minimumAge, new Actions(new DeleteAction()))))); } private T sendRequest(final Request request, final Class responseType) throws IOException { final var response = client.performRequest(request); // buffer the complete response in memory before parsing it; this will give us a better error // message which contains the raw response should the deserialization fail final var responseBody = response.getEntity().getContent().readAllBytes(); return MAPPER.readValue(responseBody, responseType); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy