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

io.kestra.plugin.aws.eventbridge.PutEvents Maven / Gradle / Ivy

The newest version!
package io.kestra.plugin.aws.eventbridge;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.executions.metrics.Counter;
import io.kestra.core.models.executions.metrics.Timer;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.runners.RunContext;
import io.kestra.core.serializers.FileSerde;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.plugin.aws.AbstractConnection;
import io.kestra.plugin.aws.ConnectionUtils;
import io.kestra.plugin.aws.eventbridge.model.Entry;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
import software.amazon.awssdk.services.eventbridge.EventBridgeClient;
import software.amazon.awssdk.services.eventbridge.model.PutEventsRequest;
import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry;
import software.amazon.awssdk.services.eventbridge.model.PutEventsResponse;
import software.amazon.awssdk.services.eventbridge.model.PutEventsResultEntry;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static io.kestra.core.utils.Rethrow.throwFunction;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Plugin(
    examples = {
        @Example(
            title = "Send multiple custom events as maps to Amazon EventBridge so that they can be matched to rules.",
            full = true,
            code = """
                id: aws_event_bridge_put_events
                namespace: company.team

                tasks:
                  - id: put_events
                    type: io.kestra.plugin.aws.eventbridge.PutEvents
                    accessKeyId: ""
                    secretKeyId: ""
                    region: "eu-central-1"
                    entries:
                      - eventBusName: "events"
                        source: "Kestra"
                        detailType: "my_object"
                        detail:
                          message: "hello from EventBridge and Kestra"
                """
        ),
        @Example(
            title = "Send multiple custom events as a JSON string to Amazon EventBridge so that they can be matched to rules.",
            full = true,
            code = """
                id: aws_event_bridge_put_events
                namespace: company.team

                tasks:
                  - id: put_events
                    type: io.kestra.plugin.aws.eventbridge.PutEvents
                    accessKeyId: ""
                    secretKeyId: ""
                    region: "eu-central-1"
                    entries:
                      - eventBusName: "events"
                        source: "Kestra"
                        detailType: "my_object"
                        detail: "{\"message\": \"hello from EventBridge and Kestra\"}"
                        resources:
                          - "arn:aws:iam::123456789012:user/johndoe"
                """
        )
    }
)
@Schema(
    title = "Send multiple custom events to Amazon EventBridge so that they can be matched to rules."
)
public class PutEvents extends AbstractConnection implements RunnableTask {
    private static final ObjectMapper MAPPER = JacksonMapper.ofIon()
        .setSerializationInclusion(JsonInclude.Include.ALWAYS);

    @PluginProperty(dynamic = false)
    @NotNull
    @Schema(
        title = "Mark the task as failed when sending an event is unsuccessful.",
        description = "If true, the task will fail when any event fails to be sent."
    )
    @Builder.Default
    private boolean failOnUnsuccessfulEvents = true;

    @PluginProperty(dynamic = true)
    @NotNull
    @Schema(
        title = "List of event entries to send to, or internal storage URI to retrieve it.",
        description = "A list of at least one EventBridge entry.",
        oneOf = {String.class, Entry[].class}
    )
    private Object entries;

    @Override
    public PutEvents.Output run(RunContext runContext) throws Exception {
        final long start = System.nanoTime();

        List entryList = readEntryList(runContext, entries);

        PutEventsResponse putEventsResponse = putEvents(runContext, entryList);

        // Set metrics
        runContext.metric(Timer.of("duration", Duration.ofNanos(System.nanoTime() - start)));
        runContext.metric(Counter.of("failedEntryCount", putEventsResponse.failedEntryCount()));
        runContext.metric(Counter.of("successfulEntryCount", entryList.size() - putEventsResponse.failedEntryCount()));
        runContext.metric(Counter.of("entryCount", entryList.size()));

        // Fail if failOnUnsuccessfulEvents
        if (failOnUnsuccessfulEvents && putEventsResponse.failedEntryCount() > 0) {
            var logger = runContext.logger();
            logger.error("Response show {} event failed: {}", putEventsResponse.failedEntryCount(), putEventsResponse);
            throw new RuntimeException(String.format("Response show %d event failed: %s", putEventsResponse.failedEntryCount(), putEventsResponse));
        }

        File tempFile = writeOutputFile(runContext, putEventsResponse, entryList);
        return Output.builder()
            .uri(runContext.storage().putFile(tempFile))
            .failedEntryCount(putEventsResponse.failedEntryCount())
            .entryCount(entryList.size())
            .build();
    }

    private File writeOutputFile(RunContext runContext, PutEventsResponse putEventsResponse, List entryList) throws IOException {
        // Create Output
        File tempFile = runContext.workingDir().createTempFile(".ion").toFile();
        try (var stream = new FileOutputStream(tempFile)) {
            List responseEntries = putEventsResponse.entries();
            for (int i = 0; i < responseEntries.size(); i++) {
                PutEventsResultEntry responseEntry = responseEntries.get(i);
                OutputEntry entry = OutputEntry.builder()
                    .entry(entryList.get(i))
                    .eventId(responseEntry.eventId())
                    .errorCode(responseEntry.errorCode())
                    .errorMessage(responseEntry.errorMessage())
                    .build();
                FileSerde.write(stream, entry);
            }
        }
        return tempFile;
    }

    private PutEventsResponse putEvents(RunContext runContext, List entryList) throws Exception {
        try (var eventBridgeClient = client(runContext)) {
            List requestEntries = entryList.stream()
                .map(throwFunction(entry -> entry.toRequestEntry(runContext)))
                .collect(Collectors.toList());

            PutEventsRequest eventsRequest = PutEventsRequest.builder()
                .entries(requestEntries)
                .build();

            return eventBridgeClient.putEvents(eventsRequest);
        }
    }

    private EventBridgeClient client(final RunContext runContext) throws IllegalVariableEvaluationException {
        final AwsClientConfig clientConfig = awsClientConfig(runContext);
        return ConnectionUtils.configureSyncClient(clientConfig, EventBridgeClient.builder()).build();
    }

    private List readEntryList(RunContext runContext, Object entries) throws IllegalVariableEvaluationException, URISyntaxException, IOException {
        if (entries instanceof String) {
            URI from = new URI(runContext.render((String) entries));
            if (!from.getScheme().equals("kestra")) {
                throw new IllegalArgumentException("Invalid entries parameter, must be a Kestra internal storage URI, or a list of entries.");
            }
            try (BufferedReader inputStream = new BufferedReader(new InputStreamReader(runContext.storage().getFile(from)))) {
                return FileSerde.readAll(inputStream, Entry.class)
                    .collectList().block();
            }
        } else if (entries instanceof List) {
            return MAPPER.convertValue(entries, new TypeReference<>() {
            });
        }

        throw new IllegalVariableEvaluationException("Invalid event type '" + entries.getClass() + "'");
    }

    @Builder
    @Getter
    public static class Output implements io.kestra.core.models.tasks.Output {

        @Schema(
            title = "The URI of the stored data.",
            description = "The successfully and unsuccessfully ingested events. " +
                "If the ingestion was successful, the entry has the event ID in it. " +
                "Otherwise, you can use the error code and error message to identify the problem with the entry."
        )
        private URI uri;

        @Schema(
            title = "The number of failed entries."
        )
        private int failedEntryCount;

        @Schema(
            title = "The total number of entries."
        )
        private int entryCount;

        @Override
        public Optional finalState() {
            return this.failedEntryCount > 0 ? Optional.of(State.Type.WARNING) : io.kestra.core.models.tasks.Output.super.finalState();
        }
    }

    @Builder
    @Getter
    public static class OutputEntry {
        @Schema(
            title = "The ID of the event."
        )
        private final String eventId;

        @Schema(
            title = "The error code that indicates why the event submission failed."
        )
        private final String errorCode;

        @Schema(
            title = "The error message that explains why the event submission failed."
        )
        private final String errorMessage;

        @Schema(
            title = "The original entry."
        )
        private final Entry entry;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy