Skip to content

AWS CloudWatch Event Sink Implementation #1965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

adnanhemani
Copy link
Contributor

As per discussion at this morning's Polaris Community Sync, I am introducing this event listener that will allow users to sink events to AWS CloudWatch Logs - where they can search through events for auditing purposes, if they wish. I do intend to introduce similar changes for Azure and GCP once this is merged so that we have parity across all CSPs - but this will help nail down the exact pattern to make this work.

Below is a screenshot of a sample event sent to CloudWatch:

Screenshot 2025-06-26 at 7 20 31 PM

@snazy
Copy link
Member

snazy commented Jun 27, 2025

Didn't look into the whole PR, but two things I noticed:

  • The PR's doing more than mentioned in the summary & description
  • There's no (integration) testing for the feature (IIRC there are solutions to test cloudwatch locally)

@adnanhemani
Copy link
Contributor Author

Hi @snazy,

The PR's doing more than mentioned in the summary & description

Yes, I'm assuming you're referring to the creation of one new event (onAfterCatalogCreated). It is much faster to use that to rapidly test new event listeners. If it is a sticking point, I'm happy to break that into a separate, few-line PR. WDYT?

There's no (integration) testing for the feature (IIRC there are solutions to test cloudwatch locally)

I didn't want to sink (no pun intended) more time trying to setup LocalStack for this repository if this approach was wildly off-the-mark. If you think the general approach is reasonable and the main things that will need change are implementation details, I will be glad to add that in the next revision. IIRC, this is also something that we only get with AWS - both Azure and GCP equivalents of CloudWatch do not have equivalent testing solutions (this could be outdated information but I can't find anything in a quick Google search). If having parity across CSPs on testing is important, maybe we settle for unit tests instead? Let me know your thoughts.

@adnanhemani
Copy link
Contributor Author

I've added a test using LocalStack, which should help emulate CloudWatch. This PR is ready for a full review.

Copy link
Member

@snazy snazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some thread-safety/concurrency issues and memory leaks in this change, which have to be addressed.

The implementation can also be simplified.

I'd also recommend to move this to its own module to streamline CI and testing in general.

Comment on lines 68 to 70
private static final String DEFAULT_LOG_STREAM_NAME = "polaris-cloudwatch-default-stream";
private static final String DEFAULT_LOG_GROUP_NAME = "polaris-cloudwatch-default-group";
private static final String DEFAULT_REGION = "us-east-1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those hard coded defaults shouldn't be there. Defaulting to an AWS region is not great and will cause user confusion. I do not mind having defaults of the log stream/group name in the Quarkus configuration, but region is awkward IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had added the defaults based on your previous advice that things should work "out of the box". Without a default region, this listener will fail for users.

Shifted the defaults to QuarkusPolarisEventListenerConfiguration.

LoggerFactory.getLogger(AwsCloudWatchEventListenerTest.class);

private static final DockerImageName LOCALSTACK_IMAGE =
DockerImageName.parse("localstack/localstack:3.4");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard coding dependency versions in source isn't good practice. This should be replaced with the container-spec-helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I'm not sure if my changes are what you meant, but please let me know if not.

@adnanhemani adnanhemani requested a review from snazy July 21, 2025 07:39

// Event overrides below
@Override
public void onAfterTableRefreshed(AfterTableRefreshedEvent event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we need to refactor these out so that the listener itself with all its Future-wrangling logic does not become exceedingly long

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done this on the new revision!

return;
}
InputLogEvent inputLogEvent = createLogEvent(eventAsJson, getCurrentTimestamp(callContext));
Future<?> future = executorService.submit(() -> sendAndHandleCloudWatchCall(inputLogEvent));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two thoughts:

  1. For this listener, I think we should offer a simple synchronous submission mode
  2. As we add more listeners, seems like some of this asynchronous handling logic will be duplicated so it might be worth refactoring it out. This can wait until we add more listeners though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. For this listener, I think we should offer a simple synchronous submission mode

I still disagree with this, to be honest. It will likely cause a sizable slowdown to fast operations. But I can add a mode that does this in-line instead. Adding to the next revision.

  1. As we add more listeners, seems like some of this asynchronous handling logic will be duplicated so it might be worth refactoring it out. This can wait until we add more listeners though.

Sounds good, will do on the next PR.

Copy link
Contributor

@eric-maynard eric-maynard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is getting really close. Left a couple of comments on the listener structure.


import java.util.HashMap;

public abstract class JsonEventListener extends PolarisEventListener {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's more like a MapBasedEventListener right? There's no JSON here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map will transform into a JSON string when it sinks, which is why I made it this name. But open to change if you think MapBasedEventListener is better?

properties.put("table_identifier", event.tableIdentifier().toString());
transformAndSendEvent(properties);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: extra whitespace

Copy link
Contributor

@eric-maynard eric-maynard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Copy link
Contributor

@singhpk234 singhpk234 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @adnanhemani for the change, it mostly LGTM !

@StaticInitSafe
@ConfigMapping(prefix = "polaris.event-listener.aws-cloudwatch")
@ApplicationScoped
public interface QuarkusAwsCloudWatchConfiguration extends AwsCloudWatchConfiguration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since its a public interface would be nice to have a doc

Comment on lines +118 to +129
try {
client.createLogGroup(CreateLogGroupRequest.builder().logGroupName(logGroup).build());
} catch (ResourceAlreadyExistsException ignored) {
LOGGER.debug("Log group {} already exists", logGroup);
}

try {
client.createLogStream(
CreateLogStreamRequest.builder().logGroupName(logGroup).logStreamName(logStream).build());
} catch (ResourceAlreadyExistsException ignored) {
LOGGER.debug("Log stream {} already exists", logStream);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we only on sdk retries for this creation ?
what if we create a log group but creating log stream fails, are we ok overall ?

# Dockerfile to provide the image name and tag to a test.
# Version is managed by Renovate - do not edit.
FROM localstack/localstack:3.4

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra line ?

@Override
protected void transformAndSendEvent(HashMap<String, Object> properties) {
properties.put("realm", callContext.getRealmContext().getRealmIdentifier());
properties.put("principal", securityContext.getUserPrincipal().getName());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about also adding the activated prinicpal roles too ?

EventWithRetry currValue = futures.remove(future);
if (currFutureState.equals(Future.State.FAILED)
|| currFutureState.equals(Future.State.CANCELLED)) {
if (currValue.retryCount >= 3) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[good to have] is it possible to make this retry count configurable ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy