Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packer/amazon-cloudwatch-agent.json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"metrics_collection_interval": 60,
"logfile": "/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log"
},

"logs": {
"logs_collected": {
"files": {
Expand All @@ -30,6 +29,7 @@
},

"metrics": {
"namespace": "CSYE6225_WebApp",
"append_dimensions": {
"InstanceId": "$${aws:InstanceId}",
"AutoScalingGroupName": "$${aws:AutoScalingGroupName}"
Expand All @@ -53,6 +53,10 @@
{"name": "disk_used_percent", "rename": "DiskUsage", "unit": "Percent"}
],
"metrics_collection_interval": 300
},
"statsd": {
"service_address": ":8125",
"metrics_collection_interval": 60
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packer/builds.pkr.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ EOC
inline = [
"set -euo pipefail",
"sudo mv /tmp/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json",
"sudo cp /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json.bk",
"sudo chown root:root /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json",
"sudo chmod 0644 /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json",
"sudo systemctl enable amazon-cloudwatch-agent"
Expand Down
124 changes: 124 additions & 0 deletions src/main/java/com/isaactai/cloudnativeweb/aop/MetricsAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.isaactai.cloudnativeweb.config;

import com.isaactai.cloudnativeweb.metrics.ApiObserved;
import com.isaactai.cloudnativeweb.metrics.ApiResourceTag;
import com.isaactai.cloudnativeweb.metrics.S3Observed;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerMapping;

import java.util.ArrayList;
import java.util.List;

/**
* @author tisaac
*/
@Aspect
@Component
@RequiredArgsConstructor
public class MetricsAspect {
private final MeterRegistry registry;

@Around("@annotation(apiObs)")
public Object apiAround(ProceedingJoinPoint pjp, ApiObserved apiObs) throws Throwable {
long start = System.nanoTime();
boolean success = true;
try {
return pjp.proceed(); // Proceed with the original method call (Controller handler)
} catch (Throwable t) {
success = false;
throw t;
} finally {
long ns = System.nanoTime() - start;
double ms = ns / 1_000_000.0;

// Extract HTTP method and URI pattern from the current request
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String method = (attrs != null && attrs.getRequest() != null)
? attrs.getRequest().getMethod()
: "UNKNOWN";

String uriPattern = (attrs != null && attrs.getRequest() != null)
? String.valueOf(attrs.getRequest().getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE))
: "UNKNOWN";

// read @ApiResourceTag
MethodSignature sig = (MethodSignature) pjp.getSignature();
Class<?> cls = sig.getDeclaringType();
ApiResourceTag tag = cls.getAnnotation(ApiResourceTag.class);
if (tag == null) tag = sig.getMethod().getAnnotation(ApiResourceTag.class);

List<Tag> tags = new ArrayList<>();
tags.add(Tag.of("method", method));
tags.add(Tag.of("uri", uriPattern));
tags.add(Tag.of("outcome", success ? "success" : "error"));
if (tag != null) {
if (!tag.resource().isBlank()) tags.add(Tag.of("resource", tag.resource()));
if (!tag.tag().isBlank()) tags.add(Tag.of("custom_tag", tag.tag()));
}

Counter.builder(apiObs.name() + ".count")
.tags(tags)
.register(registry)
.increment();

// Latency: use DistributionSummary to record the value directly in milliseconds
// (Timer reports in seconds and uploads as sum/count/max; using Summary makes the CloudWatch chart show ms directly)
DistributionSummary.builder(apiObs.name() + ".time.ms")
.tags(tags)
.register(registry)
.record(ms);
}
}

@Around("execution(* org.springframework.data.repository.Repository+.*(..)) " +
"|| within(@org.springframework.stereotype.Repository *)")
public Object dbAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
boolean success = true;
try {
return pjp.proceed();
} catch (Throwable t) {
success = false;
throw t;
}finally {
double ms = (System.nanoTime() - start) / 1_000_000.0;
String op = pjp.getSignature().getName();
DistributionSummary.builder("app.db.query.time.ms")
.baseUnit("milliseconds")
.tag("operation", op)
.tag("success", String.valueOf(success))
.register(registry)
.record(ms);
}
}

@Around("@annotation(s3Obs)")
public Object s3Around(ProceedingJoinPoint pjp, S3Observed s3Obs) throws Throwable {
long start = System.nanoTime();
boolean success = true;
try {
return pjp.proceed();
} catch (Throwable t) {
success = false;
throw t;
} finally {
double ms = (System.nanoTime() - start) / 1_000_000.0;
DistributionSummary.builder(s3Obs.name() + ".time.ms")
.baseUnit("milliseconds")
.tag("success", String.valueOf(success))
.register(registry)
.record(ms);
}
}
}
44 changes: 0 additions & 44 deletions src/main/java/com/isaactai/cloudnativeweb/config/TimedS3.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.isaactai.cloudnativeweb.common.exception.BadRequestException;
import com.isaactai.cloudnativeweb.logging.AccessLog;
import com.isaactai.cloudnativeweb.logging.AccessNote;
import com.isaactai.cloudnativeweb.metrics.ApiObserved;
import io.micrometer.core.annotation.Timed;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -35,8 +36,8 @@ public HealthController(HealthCheckService service, HealthProbeService healthPro
clientWarn = "Health Check failed",
serverError = "Unexpected error occurred"
)
@Timed(value = "api.healthz", description = "Time taken to respond to /healthz requests")
@GetMapping("/healthz")
@ApiObserved
public ResponseEntity<Void> healthz(
HttpServletRequest request,
@RequestParam Map<String, String> queryParams
Expand Down Expand Up @@ -74,6 +75,7 @@ public ResponseEntity<Void> healthz(
label = "Health",
clientWarn = "Health Check failed - Method not allowed"
)
@ApiObserved
public ResponseEntity<Void> healthzWrongMethod() {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build();
}
Expand All @@ -88,6 +90,7 @@ public ResponseEntity<Void> healthzWrongMethod() {
clientWarn = "Health Probe failed",
serverError = "Unexpected error occurred"
)
@ApiObserved
public ResponseEntity<HealthProbeService.HealthResponse> probe(HttpServletRequest request) {
try {
if (request.getInputStream().read() != -1) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.isaactai.cloudnativeweb.image;

import com.isaactai.cloudnativeweb.metrics.ApiResourceTag;
import com.isaactai.cloudnativeweb.image.dto.ImageResponse;
import com.isaactai.cloudnativeweb.logging.AccessNote;
import io.micrometer.core.annotation.Timed;
import com.isaactai.cloudnativeweb.metrics.S3Observed;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand All @@ -18,6 +19,7 @@
@RestController
@RequestMapping("/v1/product/{product_id}/image")
@RequiredArgsConstructor
@ApiResourceTag(resource = "Image")
public class ImageController {
private final ImageService service;

Expand All @@ -29,7 +31,7 @@ public class ImageController {
clientWarn = "Image upload failed",
serverError = "Unexpected error occurred during image upload"
)
@Timed(value = "api.image.upload", description = "Time taken to upload an image")
@S3Observed
public ImageResponse uploadImage(
@PathVariable("product_id") Long productId,
@RequestParam("file") MultipartFile file,
Expand All @@ -46,7 +48,7 @@ public ImageResponse uploadImage(
clientWarn = "Image deletion failed",
serverError = "Unexpected error occurred during image deletion"
)
@Timed(value = "api.image.delete", description = "Time taken to delete an image")
@S3Observed
public void deleteImage(
@PathVariable("product_id") Long productId,
@PathVariable("image_id") Long imageId,
Expand All @@ -63,7 +65,7 @@ public void deleteImage(
clientWarn = "Image listing failed",
serverError = "Unexpected error occurred during image listing"
)
@Timed(value = "api.image.list", description = "Time taken to list images for a product")
@S3Observed
public List<ImageResponse> listImages(
@PathVariable("product_id") Long productId
) {
Expand All @@ -78,7 +80,7 @@ public List<ImageResponse> listImages(
clientWarn = "Image retrieval failed",
serverError = "Unexpected error occurred during image retrieval"
)
@Timed(value = "api.image.get", description = "Time taken to get image details")
@S3Observed
public ImageResponse getImage(
@PathVariable("product_id") Long productId,
@PathVariable("image_id") Long imageId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.isaactai.cloudnativeweb.common.exception.BadRequestException;
import com.isaactai.cloudnativeweb.common.exception.NotFoundException;
import com.isaactai.cloudnativeweb.config.TimedS3;
import com.isaactai.cloudnativeweb.image.dto.ImageResponse;
import com.isaactai.cloudnativeweb.image.exception.S3UploadException;
import com.isaactai.cloudnativeweb.product.Product;
Expand All @@ -16,6 +15,7 @@
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;

import java.io.IOException;
Expand All @@ -33,7 +33,7 @@ public class ImageService {
private final ImageRepository repo;
private final UserService userService;
private final ProductService prodService;
private final TimedS3 timedS3;
private final S3Client s3;

@Value("${aws.s3.bucket}") // read bucket name from .env
private String bucketName;
Expand All @@ -59,7 +59,7 @@ public ImageResponse uploadProdImg(String name, Long productId, MultipartFile fi
user.getId(), productId, UUID.randomUUID(), safeName);

try {
timedS3.putObject(
s3.putObject(
PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
Expand Down Expand Up @@ -103,7 +103,7 @@ public void deleteForUser(String username, Long productId, Long imageId) {
}

try {
timedS3.deleteObject(DeleteObjectRequest.builder()
s3.deleteObject(DeleteObjectRequest.builder()
.bucket(bucketName)
.key(img.getS3BucketPath())
.build());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.isaactai.cloudnativeweb.config;
package com.isaactai.cloudnativeweb.logging;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/isaactai/cloudnativeweb/metrics/ApiObserved.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.isaactai.cloudnativeweb.metrics;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiObserved {
String name() default "app.api";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.isaactai.cloudnativeweb.metrics;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author tisaac
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiResourceTag {
String resource() default "";
String tag() default "";
}
12 changes: 12 additions & 0 deletions src/main/java/com/isaactai/cloudnativeweb/metrics/S3Observed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.isaactai.cloudnativeweb.metrics;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface S3Observed {
String name() default "app.s3";
}
Loading
Loading