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

io.camunda.zeebe.exporter.opensearch.OpensearchExporter Maven / Gradle / Ivy

There is a newer version: 8.7.0-alpha1
Show newest version
/*
 * 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.opensearch;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.camunda.search.connect.plugin.PluginRepository;
import io.camunda.zeebe.exporter.api.Exporter;
import io.camunda.zeebe.exporter.api.ExporterException;
import io.camunda.zeebe.exporter.api.context.Context;
import io.camunda.zeebe.exporter.api.context.Controller;
import io.camunda.zeebe.exporter.opensearch.OpensearchExporterConfiguration.IndexConfiguration;
import io.camunda.zeebe.protocol.record.Record;
import io.camunda.zeebe.protocol.record.RecordType;
import io.camunda.zeebe.protocol.record.ValueType;
import io.micrometer.core.instrument.MeterRegistry;
import java.io.IOException;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OpensearchExporter implements Exporter {

  // by default, the bulk request may not be bigger than 100MB
  private static final int RECOMMENDED_MAX_BULK_MEMORY_LIMIT = 100 * 1024 * 1024;

  private Logger log = LoggerFactory.getLogger(getClass().getPackageName());
  private final ObjectMapper exporterMetadataObjectMapper = new ObjectMapper();

  private final OpensearchExporterMetadata exporterMetadata = new OpensearchExporterMetadata();
  private final PluginRepository pluginRepository = new PluginRepository();

  private Controller controller;
  private OpensearchExporterConfiguration configuration;
  private OpensearchClient client;
  private OpensearchRecordCounters recordCounters;
  private MeterRegistry meterRegistry;

  private long lastPosition = -1;
  private boolean indexTemplatesCreated;

  @Override
  public void configure(final Context context) {
    log = context.getLogger();
    configuration = context.getConfiguration().instantiate(OpensearchExporterConfiguration.class);
    log.debug("Exporter configured with {}", configuration);

    validate(configuration);
    pluginRepository.load(configuration.getInterceptorPlugins());

    context.setFilter(new OpensearchRecordFilter(configuration));
    indexTemplatesCreated = false;
    meterRegistry = context.getMeterRegistry();
  }

  @Override
  public void open(final Controller controller) {
    this.controller = controller;
    client = createClient();

    recordCounters =
        controller
            .readMetadata()
            .map(this::deserializeExporterMetadata)
            .map(OpensearchExporterMetadata::getRecordCountersByValueType)
            .map(OpensearchRecordCounters::new)
            .orElse(new OpensearchRecordCounters());

    scheduleDelayedFlush();
    log.info("Exporter opened");
  }

  @Override
  public void close() {

    try {
      flush();
      updateLastExportedPosition();
    } catch (final Exception e) {
      log.warn("Failed to flush records before closing exporter.", e);
    }

    try {
      client.close();
    } catch (final Exception e) {
      log.warn("Failed to close opensearch client", e);
    }

    try {
      pluginRepository.close();
    } catch (final Exception e) {
      log.warn("Failed to close plugin repository", e);
    }

    log.info("Exporter closed");
  }

  @Override
  public void export(final Record record) {
    if (!indexTemplatesCreated) {
      createIndexTemplates();

      updateRetentionPolicyForExistingIndices();
    }

    final var recordSequence = recordCounters.getNextRecordSequence(record);
    client.index(record, recordSequence);
    lastPosition = record.getPosition();

    if (client.shouldFlush()) {
      flush();
      // Update the record counters only after the flush was successful. If the synchronous flush
      // fails then the exporter will be invoked with the same record again.
      recordCounters.updateRecordCounters(record, recordSequence);
      updateLastExportedPosition();
    } else {
      // If the exporter doesn't flush synchronously then it can update the record counters
      // immediately. If the asynchronous flush fails then it will retry only the flush operation
      // with the records in the pending bulk request.
      recordCounters.updateRecordCounters(record, recordSequence);
    }
  }

  private void validate(final OpensearchExporterConfiguration configuration) {
    if (configuration.index.prefix != null && configuration.index.prefix.contains("_")) {
      throw new ExporterException(
          String.format(
              "Opensearch prefix must not contain underscore. Current value: %s",
              configuration.index.prefix));
    }

    if (configuration.bulk.memoryLimit > RECOMMENDED_MAX_BULK_MEMORY_LIMIT) {
      log.warn(
          "The bulk memory limit is set to more than {} bytes. It is recommended to set the limit between 5 to 15 MB.",
          RECOMMENDED_MAX_BULK_MEMORY_LIMIT);
    }

    final Integer numberOfShards = configuration.index.getNumberOfShards();
    if (numberOfShards != null && numberOfShards < 1) {
      throw new ExporterException(
          String.format(
              "Opensearch numberOfShards must be >= 1. Current value: %d", numberOfShards));
    }

    final Integer numberOfReplicas = configuration.index.getNumberOfReplicas();
    if (numberOfReplicas != null && numberOfReplicas < 0) {
      throw new ExporterException(
          String.format(
              "Opensearch numberOfReplicas must be >= 0. Current value: %d", numberOfReplicas));
    }
  }

  // TODO: remove this and instead allow client to be inject-able for testing
  protected OpensearchClient createClient() {
    return new OpensearchClient(
        configuration,
        meterRegistry,
        RestClientFactory.of(configuration, pluginRepository.asRequestInterceptor()));
  }

  private void flushAndReschedule() {
    try {
      flush();
      updateLastExportedPosition();
    } catch (final Exception e) {
      log.warn("Unexpected exception occurred on periodically flushing bulk, will retry later.", e);
    }
    scheduleDelayedFlush();
  }

  private void scheduleDelayedFlush() {
    controller.scheduleCancellableTask(
        Duration.ofSeconds(configuration.bulk.delay), this::flushAndReschedule);
  }

  private void flush() {
    client.flush();
  }

  private void updateLastExportedPosition() {
    exporterMetadata.setRecordCountersByValueType(recordCounters.getRecordCounters());
    final var serializeExporterMetadata = serializeExporterMetadata(exporterMetadata);
    controller.updateLastExportedRecordPosition(lastPosition, serializeExporterMetadata);
  }

  private byte[] serializeExporterMetadata(final OpensearchExporterMetadata metadata) {
    try {
      return exporterMetadataObjectMapper.writeValueAsBytes(metadata);
    } catch (final JsonProcessingException e) {
      throw new OpensearchExporterException("Failed to serialize exporter metadata", e);
    }
  }

  private OpensearchExporterMetadata deserializeExporterMetadata(final byte[] metadata) {
    try {
      return exporterMetadataObjectMapper.readValue(metadata, OpensearchExporterMetadata.class);
    } catch (final IOException e) {
      throw new OpensearchExporterException("Failed to deserialize exporter metadata", e);
    }
  }

  private void createIndexTemplates() {
    if (configuration.retention.isEnabled()) {
      createIndexStateManagementPolicy();
    } else {
      deleteIndexStateManagementPolicy();
    }

    final IndexConfiguration index = configuration.index;

    if (index.createTemplate) {
      createComponentTemplate();

      if (index.deployment) {
        createValueIndexTemplate(ValueType.DEPLOYMENT);
      }
      if (index.process) {
        createValueIndexTemplate(ValueType.PROCESS);
      }
      if (index.error) {
        createValueIndexTemplate(ValueType.ERROR);
      }
      if (index.incident) {
        createValueIndexTemplate(ValueType.INCIDENT);
      }
      if (index.job) {
        createValueIndexTemplate(ValueType.JOB);
      }
      if (index.jobBatch) {
        createValueIndexTemplate(ValueType.JOB_BATCH);
      }
      if (index.message) {
        createValueIndexTemplate(ValueType.MESSAGE);
      }
      if (index.messageBatch) {
        createValueIndexTemplate(ValueType.MESSAGE_BATCH);
      }
      if (index.messageSubscription) {
        createValueIndexTemplate(ValueType.MESSAGE_SUBSCRIPTION);
      }
      if (index.variable) {
        createValueIndexTemplate(ValueType.VARIABLE);
      }
      if (index.variableDocument) {
        createValueIndexTemplate(ValueType.VARIABLE_DOCUMENT);
      }
      if (index.processInstance) {
        createValueIndexTemplate(ValueType.PROCESS_INSTANCE);
      }
      if (index.processInstanceBatch) {
        createValueIndexTemplate(ValueType.PROCESS_INSTANCE_BATCH);
      }
      if (index.processInstanceCreation) {
        createValueIndexTemplate(ValueType.PROCESS_INSTANCE_CREATION);
      }
      if (index.processInstanceMigration) {
        createValueIndexTemplate(ValueType.PROCESS_INSTANCE_MIGRATION);
      }
      if (index.processInstanceModification) {
        createValueIndexTemplate(ValueType.PROCESS_INSTANCE_MODIFICATION);
      }
      if (index.processMessageSubscription) {
        createValueIndexTemplate(ValueType.PROCESS_MESSAGE_SUBSCRIPTION);
      }
      if (index.decisionRequirements) {
        createValueIndexTemplate(ValueType.DECISION_REQUIREMENTS);
      }
      if (index.decision) {
        createValueIndexTemplate(ValueType.DECISION);
      }
      if (index.decisionEvaluation) {
        createValueIndexTemplate(ValueType.DECISION_EVALUATION);
      }
      if (index.checkpoint) {
        createValueIndexTemplate(ValueType.CHECKPOINT);
      }
      if (index.timer) {
        createValueIndexTemplate(ValueType.TIMER);
      }
      if (index.messageStartEventSubscription) {
        createValueIndexTemplate(ValueType.MESSAGE_START_EVENT_SUBSCRIPTION);
      }
      if (index.processEvent) {
        createValueIndexTemplate(ValueType.PROCESS_EVENT);
      }
      if (index.deploymentDistribution) {
        createValueIndexTemplate(ValueType.DEPLOYMENT_DISTRIBUTION);
      }
      if (index.escalation) {
        createValueIndexTemplate(ValueType.ESCALATION);
      }
      if (index.signal) {
        createValueIndexTemplate(ValueType.SIGNAL);
      }
      if (index.signalSubscription) {
        createValueIndexTemplate(ValueType.SIGNAL_SUBSCRIPTION);
      }
      if (index.resourceDeletion) {
        createValueIndexTemplate(ValueType.RESOURCE_DELETION);
      }
      if (index.commandDistribution) {
        createValueIndexTemplate(ValueType.COMMAND_DISTRIBUTION);
      }
      if (index.form) {
        createValueIndexTemplate(ValueType.FORM);
      }
      if (index.userTask) {
        createValueIndexTemplate(ValueType.USER_TASK);
      }
      if (index.compensationSubscription) {
        createValueIndexTemplate(ValueType.COMPENSATION_SUBSCRIPTION);
      }
      if (index.messageCorrelation) {
        createValueIndexTemplate(ValueType.MESSAGE_CORRELATION);
      }
      if (index.user) {
        createValueIndexTemplate(ValueType.USER);
      }

      if (index.authorization) {
        createValueIndexTemplate(ValueType.AUTHORIZATION);
      }
    }

    indexTemplatesCreated = true;
  }

  private void createIndexStateManagementPolicy() {
    final var policyOptional = client.getIndexStateManagementPolicy();

    // Create the policy if it doesn't exist yet
    if (policyOptional.isEmpty()) {
      if (!client.createIndexStateManagementPolicy()) {
        log.warn("Failed to acknowledge the creation of the Index State Management Policy");
      }
      return;
    }

    // Update the policy if it exists and is different from the configuration
    final var policy = policyOptional.get();
    if (!policy.equalsConfiguration(configuration)) {
      if (!client.updateIndexStateManagementPolicy(policy.seqNo(), policy.primaryTerm())) {
        log.warn("Failed to acknowledge the update of the Index State Management Policy");
      }
    }
  }

  private void deleteIndexStateManagementPolicy() {
    final var policyOptional = client.getIndexStateManagementPolicy();
    if (policyOptional.isEmpty()) {
      return;
    }

    if (!client.deleteIndexStateManagementPolicy()) {
      log.warn("Failed to acknowledge the deletion of the Index State Management Policy");
    }
  }

  private void createComponentTemplate() {
    if (!client.putComponentTemplate()) {
      log.warn("Failed to acknowledge the creation or update of the component template");
    }
  }

  private void createValueIndexTemplate(final ValueType valueType) {
    if (!client.putIndexTemplate(valueType)) {
      log.warn(
          "Failed to acknowledge the creation or update of the index template for value type {}",
          valueType);
    }
  }

  private void updateRetentionPolicyForExistingIndices() {
    final boolean successful;
    if (configuration.retention.isEnabled()) {
      successful = client.bulkAddISMPolicyToAllZeebeIndices();
    } else {
      successful = client.bulkRemoveISMPolicyToAllZeebeIndices();
    }

    if (!successful) {
      log.warn("Failed to acknowledge the the update of retention policy for existing indices");
    }
  }

  private static class OpensearchRecordFilter implements Context.RecordFilter {

    private final OpensearchExporterConfiguration configuration;

    OpensearchRecordFilter(final OpensearchExporterConfiguration configuration) {
      this.configuration = configuration;
    }

    @Override
    public boolean acceptType(final RecordType recordType) {
      return configuration.shouldIndexRecordType(recordType);
    }

    @Override
    public boolean acceptValue(final ValueType valueType) {
      return configuration.shouldIndexValueType(valueType);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy