Skip to content

Commit

Permalink
Merge pull request #1159 from kusumachalasani/cloudwatchlogs
Browse files Browse the repository at this point in the history
Add cloudwatch LogAppender
  • Loading branch information
dinogun authored Apr 18, 2024
2 parents ee25ddb + 1d2b0ba commit 3216832
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 3 deletions.
26 changes: 26 additions & 0 deletions design/KruizeConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,32 @@ The following environment variables are set using the `kubectl apply` command wi
- Description: Timezone configuration for Hibernate.
- Value: "UTC"

## CloudWatch Configuration

- **accessKeyId**
- Description: AWS account's access key ID. If not provided, CloudWatch logging is disabled.
- Value: ""

- **secretAccessKey**
- Description: AWS account's secret access key. If not provided, CloudWatch logging is disabled.
- Value: ""

- **region**
- Description: AWS region where CloudWatch logs are stored. If not provided, CloudWatch logging is disabled.
- Value: ""

- **logGroup**
- Description: Name of the CloudWatch log group. Defaults to "kruize-logs".
- Value: "kruize-logs"

- **logStream**
- Description: Name of the CloudWatch log stream within the log group. Defaults to "kruize-stream".
- Value: "kruize-stream"

- **logLevel**
- Description: The minimum level of log events to send to CloudWatch. Defaults to "INFO".
- Value: "INFO"

## Other Configuration

- **deletepartitionsthreshold**
Expand Down
8 changes: 8 additions & 0 deletions design/KruizeConfigurationProcedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ data:
"c3p0maxstatements": 50,
"hbm2ddlauto": "update",
"showsql": "true"
},
"cloudwatch": {
"accessKeyId": "",
"secretAccessKey": "",
"region": "",
"logGroup": "kruize-logs",
"logStream": "kruize-stream",
"logLevel": "INFO"
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ data:
"hbm2ddlauto": "update",
"showsql": "false",
"timezone": "UTC"
},
"cloudwatch": {
"accessKeyId": "",
"logGroup": "kruize-logs",
"logStream": "kruize-stream",
"region": "",
"secretAccessKey": "",
"logLevel": "INFO"
}
}
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ data:
"hbm2ddlauto": "none",
"showsql": "false",
"timezone": "UTC"
},
"cloudwatch": {
"accessKeyId": "",
"logGroup": "kruize-logs",
"logStream": "kruize-stream",
"region": "",
"secretAccessKey": "",
"logLevel": "INFO"
}
}
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ data:
"showsql": "false",
"timezone": "UTC"
},
"cloudwatch": {
"accessKeyId": "",
"logGroup": "kruize-logs",
"logStream": "kruize-stream",
"region": "",
"secretAccessKey": "",
"logLevel": "INFO"
},
"datasource": [
{
"name": "prometheus-1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ data:
"showsql": "false",
"timezone": "UTC"
},
"cloudwatch": {
"accessKeyId": "",
"logGroup": "kruize-logs",
"logStream": "kruize-stream",
"region": "",
"secretAccessKey": "",
"logLevel": "INFO"
},
"datasource": [
{
"name": "prometheus-1",
Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<hibernatecp30-version>6.1.7.Final</hibernatecp30-version>
<hibernate-Validator>8.0.1.Final</hibernate-Validator>
<micrometer-version>1.9.9</micrometer-version>
<awssdk-version>2.17.102</awssdk-version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -114,6 +115,12 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudwatchlogs</artifactId>
<version>${awssdk-version}</version>
</dependency>

<dependency>
<groupId>com.udojava</groupId>
<artifactId>EvalEx</artifactId>
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/autotune/Autotune.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
import com.autotune.operator.KruizeDeploymentInfo;
import com.autotune.service.HealthService;
import com.autotune.service.InitiateListener;
import com.autotune.utils.KruizeConstants;
import com.autotune.utils.MetricsConfig;
import com.autotune.utils.ServerContext;
import com.autotune.utils.*;
import com.autotune.utils.filter.KruizeCORSFilter;
import io.prometheus.client.exporter.MetricsServlet;
import io.prometheus.client.hotspot.DefaultExports;
Expand Down Expand Up @@ -103,6 +101,8 @@ public static void main(String[] args) {

try {
InitializeDeployment.setup_deployment_info();
// Configure AWS CloudWatch
CloudWatchAppender.configureLoggerForCloudWatchLog();
// Read and execute the DDLs here
executeDDLs(AnalyzerConstants.ROS_DDL_SQL);
if (KruizeDeploymentInfo.local == true) {
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/autotune/operator/KruizeDeploymentInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ public class KruizeDeploymentInfo {
public static String database_admin_username;
public static String database_admin_password;
public static String database_ssl_mode;

public static String cloudwatch_logs_access_key_id;
public static String cloudwatch_logs_secret_access_key;
public static String cloudwatch_logs_log_group;
public static String cloudwatch_logs_region;
public static String cloudwatch_logs_log_level;
public static String cloudwatch_logs_log_stream;

public static Boolean settings_save_to_db;
public static String em_only_mode;
public static Integer bulk_update_results_limit = 100;
Expand Down
168 changes: 168 additions & 0 deletions src/main/java/com/autotune/utils/CloudWatchAppender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package com.autotune.utils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.filter.AbstractFilter;
import org.apache.logging.log4j.core.layout.PatternLayout;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClient;
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient;
import software.amazon.awssdk.services.cloudwatchlogs.model.*;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static com.autotune.operator.KruizeDeploymentInfo.*;

public class CloudWatchAppender extends AbstractAppender {
private final String logGroupName;
private final String logStreamName;
private final CloudWatchLogsAsyncClient cloudWatchLogsClient;
private String sequenceToken = null;

public CloudWatchAppender(String name, Filter filter, Layout<?> layout, String logGroupName, String logStreamName, String region, String awsAccessKeyId, String awsSecretKey) {
super(name, filter, layout, false, null);
this.logGroupName = logGroupName;
this.logStreamName = logStreamName;
this.cloudWatchLogsClient = CloudWatchLogsAsyncClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(awsAccessKeyId, awsSecretKey)))
.region(Region.of(region))
.build();
}

@Override
public void start() {
super.start();
}

@Override
public void append(LogEvent event) {
String message = getLayout().toSerializable(event).toString();
List<InputLogEvent> logEvents = new ArrayList<>();
logEvents.add(InputLogEvent.builder()
.timestamp(event.getTimeMillis())
.message(message)
.build());

PutLogEventsRequest request = PutLogEventsRequest.builder()
.logGroupName(logGroupName)
.logStreamName(logStreamName)
.logEvents(logEvents)
.sequenceToken(sequenceToken)
.build();

CompletableFuture<PutLogEventsResponse> futureResponse = cloudWatchLogsClient.putLogEvents(request);
futureResponse.whenComplete((response, error) -> {
if (error != null) {
error.printStackTrace();
} else {
sequenceToken = response.nextSequenceToken();
}
});
}

public static void configureLoggerForCloudWatchLog() {
if (cloudwatch_logs_access_key_id != null && !cloudwatch_logs_access_key_id.isEmpty() && cloudwatch_logs_secret_access_key != null && !cloudwatch_logs_secret_access_key.isEmpty() && cloudwatch_logs_region != null && !cloudwatch_logs_region.isEmpty()) {
try {
// Define default values for attributes if they are empty or null
String cw_logs_log_group = cloudwatch_logs_log_group == null || cloudwatch_logs_log_group.isEmpty() ? "kruize-logs" : cloudwatch_logs_log_group;
String cw_logs_log_stream = cloudwatch_logs_log_stream == null || cloudwatch_logs_log_stream.isEmpty() ? "kruize-stream" : cloudwatch_logs_log_stream;
String cw_logs_log_level = cloudwatch_logs_log_level == null || cloudwatch_logs_log_level.isEmpty() ? "INFO" : cloudwatch_logs_log_level;
String cw_logs_log_level_uc = cw_logs_log_level.toUpperCase();


CloudWatchLogsClient logsClient = CloudWatchLogsClient.builder()
.region(Region.of(cloudwatch_logs_region))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(cloudwatch_logs_access_key_id, cloudwatch_logs_secret_access_key)))
.build();

if (!logGroupExists(logsClient, cw_logs_log_group)) {
createLogGroup(logsClient, cw_logs_log_group);
}

if (!logStreamExists(logsClient, cw_logs_log_group, cw_logs_log_stream)) {
createLogStream(logsClient, cw_logs_log_group, cw_logs_log_stream);
}

LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();

Level level = Level.getLevel(cw_logs_log_level_uc);
Filter filter = new LogFilter(level);
Layout<?> layout = PatternLayout.newBuilder().withPattern(KruizeConstants.Patterns.CLOUDWATCH_LOG_PATTERN).build();
CloudWatchAppender appender = new CloudWatchAppender("cloudwatchRootAppender", filter, layout, cw_logs_log_group, cw_logs_log_stream, cloudwatch_logs_region,cloudwatch_logs_access_key_id,cloudwatch_logs_secret_access_key);

appender.start();
config.addAppender(appender);
// Adding CloudWatch Appender to "com.autotune" logger
LoggerConfig loggerConfig = config.getLoggerConfig("com.autotune");
loggerConfig.addAppender(appender, level, filter);
context.updateLoggers(config);

} catch (Exception e) {
LOGGER.error(e.getMessage());
}
} else {
LOGGER.info("AWS access details are not provided. Skipping sending logs to CloudWatch.");
}
}
private static boolean logGroupExists(CloudWatchLogsClient logsClient, String logGroupName) {
DescribeLogGroupsRequest request = DescribeLogGroupsRequest.builder()
.logGroupNamePrefix(logGroupName)
.build();
DescribeLogGroupsResponse response = logsClient.describeLogGroups(request);
List<LogGroup> logGroups = response.logGroups();
return logGroups.stream().anyMatch(group -> group.logGroupName().equals(logGroupName));
}

private static boolean logStreamExists(CloudWatchLogsClient logsClient, String logGroupName, String logStreamName) {
DescribeLogStreamsRequest request = DescribeLogStreamsRequest.builder()
.logGroupName(logGroupName)
.logStreamNamePrefix(logStreamName)
.build();
DescribeLogStreamsResponse response = logsClient.describeLogStreams(request);
List<LogStream> logStreams = response.logStreams();
return logStreams.stream().anyMatch(stream -> stream.logStreamName().equals(logStreamName));
}

private static void createLogGroup(CloudWatchLogsClient logsClient, String logGroupName) {
CreateLogGroupRequest request = CreateLogGroupRequest.builder()
.logGroupName(logGroupName)
.build();
logsClient.createLogGroup(request);
LOGGER.info("Created log group: {}", logGroupName);
}

private static void createLogStream(CloudWatchLogsClient logsClient, String logGroupName, String logStreamName) {
CreateLogStreamRequest request = CreateLogStreamRequest.builder()
.logGroupName(logGroupName)
.logStreamName(logStreamName)
.build();
logsClient.createLogStream(request);
LOGGER.info("Created log stream: {} in log group: {}", logStreamName, logGroupName);
}


private static class LogFilter extends AbstractFilter {
private final Level level;

protected LogFilter(Level level) {
super(Result.ACCEPT, Result.DENY);
this.level = level;
}

@Override
public Result filter(LogEvent event) {
return event.getLevel().isMoreSpecificThan(level) ? onMatch : onMismatch;
}

}
}
7 changes: 7 additions & 0 deletions src/main/java/com/autotune/utils/KruizeConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ public static class Patterns {
public static final String DURATION_PATTERN = "(\\d+)([a-zA-Z]+)";
public static final String WHITESPACE_PATTERN = "\\s";
public static final String QUERY_WITH_TIME_RANGE_PATTERN = ".*\\[(\\d+)([a-zA-Z]+)\\].*";
public static final String CLOUDWATCH_LOG_PATTERN = "%d{yyyy-MM-ddHH:mm:ss.SSS} %level [%t][%F(%L)]-%msg%n";

private Patterns() {
}
Expand Down Expand Up @@ -583,6 +584,12 @@ public static final class KRUIZE_CONFIG_ENV_NAME {
public static final String SETTINGS_HIBERNATE_SHOW_SQL = "hibernate_showsql";
public static final String SETTINGS_HIBERNATE_TIME_ZONE = "hibernate_timezone";
public static final String PLOTS = "plots";
public static final String CLOUDWATCH_LOGS_ACCESS_KEY_ID = "cloudwatch_accessKeyId";
public static final String CLOUDWATCH_LOGS_SECRET_ACCESS_KEY = "cloudwatch_secretAccessKey";
public static final String CLOUDWATCH_LOGS_LOG_GROUP = "cloudwatch_logGroup";
public static final String CLOUDWATCH_LOGS_REGION = "cloudwatch_region";
public static final String CLOUDWATCH_LOGS_LOG_STREAM = "cloudwatch_logStream";
public static final String CLOUDWATCH_LOGS_LOG_LEVEL = "cloudwatch_logLevel";
public static final String LOCAL = "local";
}

Expand Down

0 comments on commit 3216832

Please sign in to comment.