diff --git a/README.md b/README.md index f88f9873..9b7365f3 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ Using the same method you can also change - Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md) - Change the max file size allowed through the server with the environment variable MAX_FILE_SIZE. default 2000MB - Customise static files such as app logo by placing files in the /customFiles/static/ directory. Example to customise app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF +- Enable/Disable metric api endpoints with ENABLE_API_METRICS. Default enabled ## API For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation diff --git a/build.gradle b/build.gradle index 832cbc2d..dcc53c3e 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'stirling.software' -version = '0.12.1' +version = '0.12.2' sourceCompatibility = '17' repositories { diff --git a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index 70235df3..ab18b1b5 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -1,8 +1,16 @@ package stirling.software.SPDF.controller.web; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -14,6 +22,8 @@ import io.micrometer.core.instrument.MeterRegistry; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.PostConstruct; +import stirling.software.SPDF.config.StartupApplicationListener; @RestController @RequestMapping("/api/v1") @@ -22,6 +32,20 @@ public class MetricsController { private final MeterRegistry meterRegistry; + private boolean isEndpointEnabled; + + @PostConstruct + public void init() { + String isEndpointEnabled = System.getProperty("ENABLE_API_METRICS"); + if (isEndpointEnabled == null) { + isEndpointEnabled = System.getenv("ENABLE_API_METRICS"); + if (isEndpointEnabled == null) { + isEndpointEnabled = "true"; + } + } + this.isEndpointEnabled = "true".equalsIgnoreCase(isEndpointEnabled); + } + public MetricsController(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @@ -29,18 +53,25 @@ public class MetricsController { @GetMapping("/status") @Operation(summary = "Application status and version", description = "This endpoint returns the status of the application and its version number.") - public Map getStatus() { + public ResponseEntity getStatus() { + if (!isEndpointEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + Map status = new HashMap<>(); status.put("status", "UP"); status.put("version", getClass().getPackage().getImplementationVersion()); - return status; + return ResponseEntity.ok(status); } @GetMapping("/loads") @Operation(summary = "GET request count", description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") - public Double getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { - try { + public ResponseEntity getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { + if (!isEndpointEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + try { double count = 0.0; @@ -68,36 +99,165 @@ public class MetricsController { } } - return count; + return ResponseEntity.ok(count); } catch (Exception e) { - return -1.0; + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + @GetMapping("/loads/all") + @Operation(summary = "GET requests count for all endpoints", + description = "This endpoint returns the count of GET requests for each endpoint.") + public ResponseEntity getAllEndpointLoads() { + if (!isEndpointEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + try { + Map counts = new HashMap<>(); + + for (Meter meter : meterRegistry.getMeters()) { + if (meter.getId().getName().equals("http.requests")) { + String method = meter.getId().getTag("method"); + if (method != null && method.equals("GET")) { + String uri = meter.getId().getTag("uri"); + if (uri != null) { + double currentCount = counts.getOrDefault(uri, 0.0); + if (meter instanceof Counter) { + currentCount += ((Counter) meter).count(); + } + counts.put(uri, currentCount); + } + } + } + } + + List results = counts.entrySet().stream() + .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(results); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + public class EndpointCount { + private String endpoint; + private double count; + + public EndpointCount(String endpoint, double count) { + this.endpoint = endpoint; + this.count = count; + } + public String getEndpoint() { + return endpoint; + } + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + public double getCount() { + return count; + } + public void setCount(double count) { + this.count = count; + } + + } + + @GetMapping("/requests") @Operation(summary = "POST request count", description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") - public Double getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { - try { - Counter counter; - if (endpoint.isPresent() && !endpoint.get().isBlank()) { - if(!endpoint.get().startsWith("/")) { - endpoint = Optional.of("/" + endpoint.get()); - } - - System.out.println("loads " + endpoint.get() + " vs " + meterRegistry.get("http.requests").tags("uri", endpoint.get()).toString()); - counter = meterRegistry.get("http.requests") - .tags("method", "POST", "uri", endpoint.get()).counter(); - } else { - counter = meterRegistry.get("http.requests") - .tags("method", "POST").counter(); - } - return counter.count(); - } catch (Exception e) { - e.printStackTrace(); - return 0.0; + public ResponseEntity getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { + if (!isEndpointEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + try { + double count = 0.0; + + for (Meter meter : meterRegistry.getMeters()) { + if (meter.getId().getName().equals("http.requests")) { + String method = meter.getId().getTag("method"); + if (method != null && method.equals("POST")) { + if (endpoint.isPresent() && !endpoint.get().isBlank()) { + if (!endpoint.get().startsWith("/")) { + endpoint = Optional.of("/" + endpoint.get()); + } + if (endpoint.get().equals(meter.getId().getTag("uri"))) { + if (meter instanceof Counter) { + count += ((Counter) meter).count(); + } + } + } else { + if (meter instanceof Counter) { + count += ((Counter) meter).count(); + } + } + } + } + } + return ResponseEntity.ok(count); + } catch (Exception e) { + return ResponseEntity.ok(-1); } - } + + @GetMapping("/requests/all") + @Operation(summary = "POST requests count for all endpoints", + description = "This endpoint returns the count of POST requests for each endpoint.") + public ResponseEntity getAllPostRequests() { + if (!isEndpointEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + try { + Map counts = new HashMap<>(); + + for (Meter meter : meterRegistry.getMeters()) { + if (meter.getId().getName().equals("http.requests")) { + String method = meter.getId().getTag("method"); + if (method != null && method.equals("POST")) { + String uri = meter.getId().getTag("uri"); + if (uri != null) { + double currentCount = counts.getOrDefault(uri, 0.0); + if (meter instanceof Counter) { + currentCount += ((Counter) meter).count(); + } + counts.put(uri, currentCount); + } + } + } + } + + List results = counts.entrySet().stream() + .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(results); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + + @GetMapping("/uptime") + public ResponseEntity getUptime() { + if (!isEndpointEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + + LocalDateTime now = LocalDateTime.now(); + Duration uptime = Duration.between(StartupApplicationListener.startTime, now); + return ResponseEntity.ok(formatDuration(uptime)); + } + + private String formatDuration(Duration duration) { + long days = duration.toDays(); + long hours = duration.toHoursPart(); + long minutes = duration.toMinutesPart(); + long seconds = duration.toSecondsPart(); + return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds); + } }