├── docker ├── grafana │ ├── grafana.ini │ └── provisioning │ │ └── datasources │ │ └── ds.yaml ├── prometheus │ └── prometheus.yaml └── restmocks │ ├── broken.json │ └── available.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── scripts ├── get-certs.sh ├── run-mocks.sh └── update-certs.sh ├── README.md ├── src ├── main │ ├── java │ │ └── io │ │ │ └── spring │ │ │ └── sample │ │ │ └── opsstatus │ │ │ ├── incident │ │ │ ├── IncidentOrigin.java │ │ │ ├── IncidentStatus.java │ │ │ ├── ItemType.java │ │ │ ├── IncidentRepository.java │ │ │ ├── ReportItem.java │ │ │ └── Incident.java │ │ │ ├── availability │ │ │ ├── Availability.java │ │ │ ├── InfrastructureComponentRepository.java │ │ │ ├── Outage.java │ │ │ └── InfrastructureComponent.java │ │ │ ├── faker │ │ │ ├── DataFaker.java │ │ │ ├── OnlineGaming.java │ │ │ └── DemoDataGenerator.java │ │ │ ├── OpsStatusApplication.java │ │ │ ├── dashboard │ │ │ ├── MissingIncidentException.java │ │ │ ├── DashboardController.java │ │ │ └── ApiController.java │ │ │ ├── OpsStatusProperties.java │ │ │ ├── OpsStatusConfiguration.java │ │ │ ├── AvailabilityChecker.java │ │ │ ├── RandomDataGenerator.java │ │ │ └── checker │ │ │ └── AvailabilityCheckClient.java │ └── resources │ │ ├── application.properties │ │ ├── static │ │ └── css │ │ │ └── style.css │ │ ├── db │ │ └── migration │ │ │ └── V001__schema.sql │ │ ├── templates │ │ ├── apierror.mustache │ │ └── dashboard.mustache │ │ └── faker │ │ └── online-gaming.yml └── test │ ├── resources │ └── io │ │ └── spring │ │ └── sample │ │ └── opsstatus │ │ └── dashboard │ │ └── missing-incident-42.json │ └── java │ └── io │ └── spring │ └── sample │ └── opsstatus │ ├── dashboard │ ├── DashboardControllerTests.java │ └── ApiControllerTests.java │ ├── availability │ └── InfrastructureComponentRepositoryTests.java │ ├── checker │ └── AvailabilityCheckClientTests.java │ └── incident │ └── IncidentRepositoryTests.java ├── settings.gradle ├── .gitignore ├── certs ├── example-org.crt └── example-org.key ├── compose.yml ├── gradlew.bat └── gradlew /docker/grafana/grafana.ini: -------------------------------------------------------------------------------- 1 | [auth.anonymous] 2 | enabled = true 3 | org_role = Admin -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bclozel/ops-status/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scripts/get-certs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | openssl s_client -connect localhost:8443 /dev/null | openssl x509 -------------------------------------------------------------------------------- /scripts/run-mocks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | docker run --rm -p 3030:3030 -v ./restmocks:/app/mocks dotronglong/faker:stable -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to run this application locally 2 | 3 | You need Java 21 and Docker 4 | 5 | ```bash 6 | $ ./gradlew bootRun 7 | ``` 8 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/incident/IncidentOrigin.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | public enum IncidentOrigin { 4 | 5 | MAINTENANCE, ISSUE 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/incident/IncidentStatus.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | public enum IncidentStatus { 4 | 5 | SCHEDULED, IN_PROGRESS, RESOLVED 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/availability/Availability.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.availability; 2 | 3 | public enum Availability { 4 | 5 | OPERATIONAL, PARTIAL, UNAVAILABLE, UNKNOWN 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/incident/ItemType.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | public enum ItemType { 4 | 5 | INVESTIGATING, RESOLVED, MONITORING, SCHEDULED, IN_PROGRESS, COMPLETED 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/io/spring/sample/opsstatus/dashboard/missing-incident-42.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "about:blank", 3 | "title": "Not Found", 4 | "status": 404, 5 | "detail": "Could not find an incident with id '42'.", 6 | "instance": "/api/incidents/42" 7 | } -------------------------------------------------------------------------------- /docker/prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | evaluation_interval: 5s 4 | 5 | scrape_configs: 6 | - job_name: "spring-boot-app" 7 | metrics_path: /actuator/prometheus 8 | static_configs: 9 | - targets: [ 'host.docker.internal:8080' ] 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/faker/DataFaker.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.faker; 2 | 3 | import net.datafaker.Faker; 4 | 5 | class DataFaker extends Faker { 6 | 7 | OnlineGaming onlineGaming() { 8 | return getProvider(OnlineGaming.class, OnlineGaming::new, this); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /scripts/update-certs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "" 4 | echo "==================================================================" 5 | echo "Refresh certificate under certs/example-org.crt" 6 | echo "==================================================================" 7 | echo "" 8 | 9 | openssl req -batch -key certs/example-org.key -new -x509 -days 365 -out certs/example-org.crt 10 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/OpsStatusApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class OpsStatusApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(OpsStatusApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /docker/restmocks/broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | { 4 | "name": "delay", 5 | "args": { 6 | "duration": 2000 7 | } 8 | } 9 | ], 10 | "request": { 11 | "method": "get", 12 | "path": "\\/[a-z-]*\\/broken" 13 | }, 14 | "response": { 15 | "statusCode": 503, 16 | "headers": { 17 | "content-type": "application/json" 18 | }, 19 | "body": { 20 | "status": "DOWN" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /docker/restmocks/available.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | { 4 | "name": "delay", 5 | "args": { 6 | "duration": 300 7 | } 8 | } 9 | ], 10 | "request": { 11 | "method": "get", 12 | "path": "\\/[a-z-]*\\/available" 13 | }, 14 | "response": { 15 | "statusCode": 200, 16 | "headers": { 17 | "content-type": "application/json" 18 | }, 19 | "body": { 20 | "status": "OK" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /docker/grafana/provisioning/datasources/ds.yaml: -------------------------------------------------------------------------------- 1 | datasources: 2 | - name: prometheus 3 | type: prometheus 4 | typeName: Prometheus 5 | typeLogoUrl: /public/app/plugins/datasource/prometheus/img/prometheus_logo.svg 6 | access: proxy 7 | url: http://host.docker.internal:9090 8 | isDefault: true 9 | jsonData: 10 | httpMethod: POST 11 | exemplarTraceIdDestinations: 12 | - name: trace_id 13 | datasourceUid: tempo 14 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/availability/InfrastructureComponentRepository.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.availability; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.repository.PagingAndSortingRepository; 5 | 6 | public interface InfrastructureComponentRepository extends CrudRepository, 7 | PagingAndSortingRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | maven { url 'https://repo.spring.io/milestone' } 6 | maven { url 'https://repo.spring.io/snapshot' } 7 | } 8 | resolutionStrategy { 9 | eachPlugin { 10 | if (requested.id.id == "io.spring.javaformat") { 11 | useModule "io.spring.javaformat:spring-javaformat-gradle-plugin:${requested.version}" 12 | } 13 | } 14 | } 15 | } 16 | 17 | rootProject.name = 'ops-status' 18 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/dashboard/MissingIncidentException.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.dashboard; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ProblemDetail; 5 | import org.springframework.web.ErrorResponseException; 6 | 7 | public class MissingIncidentException extends ErrorResponseException { 8 | 9 | public MissingIncidentException(Long id) { 10 | super(HttpStatus.NOT_FOUND, ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, 11 | "Could not find an incident with id '%s'.".formatted(id)), null); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=opsstatus 2 | 3 | spring.datasource.name=opsstatus 4 | spring.datasource.username=spring 5 | spring.datasource.password=secret 6 | spring.datasource.url=jdbc:postgresql://localhost:5432/${spring.datasource.name} 7 | 8 | spring.mvc.problemdetails.enabled=true 9 | spring.threads.virtual.enabled=true 10 | 11 | management.endpoints.web.exposure.include=health,prometheus,metrics,sbom 12 | management.metrics.tags.application=${spring.application.name} 13 | management.metrics.distribution.percentiles-histogram.http.server.requests=true 14 | management.metrics.distribution.percentiles-histogram.http.client.requests=true 15 | management.prometheus.metrics.export.step=5s 16 | management.tracing.sampling.probability=1.0 17 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/OpsStatusProperties.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | @ConfigurationProperties(prefix = "opsstatus") 6 | public class OpsStatusProperties { 7 | 8 | private final Availability availability = new Availability(); 9 | 10 | public Availability getAvailability() { 11 | return this.availability; 12 | } 13 | 14 | public static class Availability { 15 | 16 | /** 17 | * Service url for availability checks. 18 | */ 19 | private String url = "http://localhost:3030"; 20 | 21 | public String getUrl() { 22 | return this.url; 23 | } 24 | 25 | public void setUrl(String url) { 26 | this.url = url; 27 | } 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/availability/Outage.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.availability; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public final class Outage { 6 | 7 | private Availability availability; 8 | 9 | private LocalDateTime startedOn; 10 | 11 | private LocalDateTime endedOn; 12 | 13 | public Availability getAvailability() { 14 | return this.availability; 15 | } 16 | 17 | public void setAvailability(Availability availability) { 18 | this.availability = availability; 19 | } 20 | 21 | public LocalDateTime getStartedOn() { 22 | return this.startedOn; 23 | } 24 | 25 | public void setStartedOn(LocalDateTime startedOn) { 26 | this.startedOn = startedOn; 27 | } 28 | 29 | public LocalDateTime getEndedOn() { 30 | return this.endedOn; 31 | } 32 | 33 | public void setEndedOn(LocalDateTime endedOn) { 34 | this.endedOn = endedOn; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/OpsStatusConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus; 2 | 3 | import io.spring.sample.opsstatus.checker.AvailabilityCheckClient; 4 | 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | import org.springframework.web.client.RestClient; 10 | 11 | @Configuration(proxyBeanMethods = false) 12 | @EnableConfigurationProperties(OpsStatusProperties.class) 13 | @EnableScheduling 14 | class OpsStatusConfiguration { 15 | 16 | @Bean 17 | AvailabilityCheckClient availabilityCheck(RestClient.Builder builder, OpsStatusProperties properties) { 18 | return new AvailabilityCheckClient(builder, properties.getAvailability().getUrl()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/incident/IncidentRepository.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | import java.util.List; 4 | import java.util.stream.Stream; 5 | 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.domain.Slice; 8 | import org.springframework.data.jdbc.repository.query.Query; 9 | import org.springframework.data.repository.CrudRepository; 10 | import org.springframework.data.repository.PagingAndSortingRepository; 11 | 12 | public interface IncidentRepository extends CrudRepository, PagingAndSortingRepository { 13 | 14 | List findAllByStatusOrderByHappenedOnDesc(IncidentStatus status); 15 | 16 | Slice findByStatusOrderByHappenedOnDesc(IncidentStatus status, Pageable pageable); 17 | 18 | @Query("SELECT * FROM incident ORDER BY happened_on DESC") 19 | Stream streamAll(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import url(https://fonts.googleapis.com/css?family=Press+Start+2P); 3 | 4 | body { 5 | padding: 0 2rem; 6 | margin: 0 2rem; 7 | } 8 | 9 | header { 10 | text-align: center; 11 | } 12 | 13 | header .logo { 14 | margin-right: 1rem; 15 | } 16 | 17 | #dashboard { 18 | max-width: 980px; 19 | margin: 0 auto; 20 | } 21 | 22 | #apierror { 23 | max-width: 980px; 24 | margin: 4em auto; 25 | } 26 | 27 | #incidents, #infrastructure { 28 | margin-bottom: 2rem; 29 | } 30 | 31 | .item { 32 | margin-bottom: 1.5rem; 33 | } 34 | 35 | .item > * { 36 | margin-bottom: 1.5rem !important; 37 | } 38 | 39 | .incident > button { 40 | 41 | } 42 | 43 | .scroll-btn.active { 44 | bottom: 25px; 45 | } 46 | 47 | .scroll-btn { 48 | position: fixed; 49 | bottom: -60px; 50 | right: 2rem; 51 | box-shadow: 0 5px 20px rgba(0, 0, 0, .6); 52 | transition: all 0.3s ease; 53 | } 54 | 55 | .scroll-btn > span { 56 | display: block; 57 | transform: rotateZ(90deg); 58 | } -------------------------------------------------------------------------------- /src/main/resources/db/migration/V001__schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE incident( 2 | id serial NOT NULL PRIMARY KEY, 3 | title VARCHAR(255), 4 | description VARCHAR(255), 5 | happened_on timestamp, 6 | origin VARCHAR(255), 7 | status VARCHAR(255) 8 | ); 9 | 10 | CREATE TABLE report_item( 11 | incident INT, 12 | incident_key INT, 13 | type VARCHAR(255), 14 | date timestamp, 15 | description VARCHAR(255), 16 | FOREIGN KEY(incident) REFERENCES incident(id) 17 | ); 18 | 19 | CREATE TABLE infrastructure_component( 20 | id serial NOT NULL PRIMARY KEY, 21 | name VARCHAR(255) UNIQUE, 22 | description VARCHAR(255), 23 | check_uri VARCHAR(255), 24 | check_token VARCHAR(255), 25 | availability VARCHAR(255), 26 | last_checked_on timestamp 27 | ); 28 | 29 | CREATE TABLE outage( 30 | infrastructure_component INT, 31 | infrastructure_component_key INT, 32 | availability VARCHAR(255), 33 | started_on timestamp, 34 | ended_on timestamp, 35 | FOREIGN KEY(infrastructure_component) REFERENCES infrastructure_component(id) 36 | ); 37 | -------------------------------------------------------------------------------- /src/main/resources/templates/apierror.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Online Gaming - {{exception.body.title}} 7 | 8 | 9 | 10 | 11 |
12 | 16 |
17 |
18 |
19 |

API Error

20 |

{{exception.body.detail}}

21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/incident/ReportItem.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public final class ReportItem { 6 | 7 | private ItemType type; 8 | 9 | private LocalDateTime date; 10 | 11 | private String description; 12 | 13 | public static ReportItem create(ItemType type, String description) { 14 | ReportItem investigation = new ReportItem(); 15 | investigation.type = type; 16 | investigation.description = description; 17 | investigation.date = LocalDateTime.now(); 18 | return investigation; 19 | } 20 | 21 | public ItemType getType() { 22 | return this.type; 23 | } 24 | 25 | public void setType(ItemType type) { 26 | this.type = type; 27 | } 28 | 29 | public LocalDateTime getDate() { 30 | return this.date; 31 | } 32 | 33 | public void setDate(LocalDateTime date) { 34 | this.date = date; 35 | } 36 | 37 | public String getDescription() { 38 | return this.description; 39 | } 40 | 41 | public void setDescription(String description) { 42 | this.description = description; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /certs/example-org.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUJ04xSw4Fb4HO70auTL+Zm30LFj4wDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMDQxNTQ1MjdaFw0yNTAz 5 | MDQxNTQ1MjdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQCi4PzSOpATj287B1LnZff667BtU59DzGrPn7jo1P50 8 | pDpHnLUUUWpKjsEo7ErdBFOf9XIx9TlDuHw2O6ttbivz0wYnyQyIhEDjXPjnuS8g 9 | QE3l7AD3P/zirta6PjhyIxjnJqKalF7TFXuL94QHBQkGokaNp+XxU8djrIQ6xu8g 10 | 6WZZmPJTvgXG2Zn55RUAzgEXBY7wzG08+APafmYRv3sz2UwuhFDc4Gxh2sidgzwx 11 | fVYRviJ6m2wRvShXI+NOpR/HP2kOKrLI3Kyk5FUttrZFl8qx7OWcBLH/PhXN01xP 12 | fGvgfO9Vx4TRQgdYPoLmVf0rnXYa+c8dq4xd5gwSe+zZAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBRqo/nnI/F3diHesWwYakd5e8WXDDAfBgNVHSMEGDAWgBRqo/nnI/F3diHe 14 | sWwYakd5e8WXDDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB1 15 | Nq8UkIPVpXYVUREgYtrj8kdppGAt3Ev9qoiE9K6sXn3Fy1Rrzoz5f6TQA9ZaIDwb 16 | KsUNlkD70Tg4srkmqI4iKCDJ+sFBPfQlzNk0nSVq23HAAXem11HEJDjdBk/SH3Nn 17 | yaArI3QASgqPNzvGuV1BIlkhKCB09qRdg7IqW+2ZC85nMGACjdjh/y9PskN1Fh07 18 | XqHQMh8Awa9KpFkLrRJQs9VBC4z9pwAOqA8FVDGcprPGGCQ+3/CoII1dklxSGr61 19 | vpdgOUDqOk2SqtUStoC6n3eNDDFW0Z2426l3ZAPd7+hC0Du+lYdNR/i6RhQ84yl8 20 | imK3A/WIebJj9JnTFHMZ 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/AvailabilityChecker.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import io.spring.sample.opsstatus.availability.Availability; 6 | import io.spring.sample.opsstatus.availability.InfrastructureComponent; 7 | import io.spring.sample.opsstatus.availability.InfrastructureComponentRepository; 8 | import io.spring.sample.opsstatus.checker.AvailabilityCheckClient; 9 | 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class AvailabilityChecker { 15 | 16 | private final AvailabilityCheckClient client; 17 | 18 | private final InfrastructureComponentRepository repository; 19 | 20 | public AvailabilityChecker(AvailabilityCheckClient client, InfrastructureComponentRepository repository) { 21 | this.client = client; 22 | this.repository = repository; 23 | } 24 | 25 | @Scheduled(fixedRate = 5000) 26 | public void checkComponents() { 27 | Iterable components = this.repository.findAll(); 28 | for (InfrastructureComponent component : components) { 29 | Availability availability = this.client.checkAvailability(component); 30 | component.setAvailability(availability); 31 | component.setLastCheckedOn(LocalDateTime.now()); 32 | } 33 | this.repository.saveAll(components); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/RandomDataGenerator.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus; 2 | 3 | import io.spring.sample.opsstatus.availability.InfrastructureComponentRepository; 4 | import io.spring.sample.opsstatus.faker.DemoDataGenerator; 5 | import io.spring.sample.opsstatus.incident.IncidentRepository; 6 | 7 | import org.springframework.boot.ApplicationArguments; 8 | import org.springframework.boot.ApplicationRunner; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | class RandomDataGenerator implements ApplicationRunner { 13 | 14 | private final IncidentRepository incidentRepository; 15 | 16 | private final InfrastructureComponentRepository infrastructureComponentRepository; 17 | 18 | private final DemoDataGenerator demoDataGenerator; 19 | 20 | RandomDataGenerator(IncidentRepository incidentRepository, 21 | InfrastructureComponentRepository infrastructureComponentRepository) { 22 | this.incidentRepository = incidentRepository; 23 | this.infrastructureComponentRepository = infrastructureComponentRepository; 24 | this.demoDataGenerator = new DemoDataGenerator(); 25 | } 26 | 27 | @Override 28 | public void run(ApplicationArguments args) { 29 | if (this.incidentRepository.count() == 0) { 30 | this.incidentRepository.saveAll(this.demoDataGenerator.generatePastIncidents(10)); 31 | this.incidentRepository.save(this.demoDataGenerator.generateMaintenanceUpdate()); 32 | this.infrastructureComponentRepository.saveAll(this.demoDataGenerator.infrastructureComponents(5)); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/dashboard/DashboardController.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.dashboard; 2 | 3 | import io.spring.sample.opsstatus.availability.InfrastructureComponentRepository; 4 | import io.spring.sample.opsstatus.incident.IncidentRepository; 5 | import io.spring.sample.opsstatus.incident.IncidentStatus; 6 | 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.domain.Sort; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | 13 | @Controller 14 | public class DashboardController { 15 | 16 | private final IncidentRepository incidentRepository; 17 | 18 | private final InfrastructureComponentRepository infrastructureComponentRepository; 19 | 20 | public DashboardController(IncidentRepository incidentRepository, 21 | InfrastructureComponentRepository infrastructureComponentRepository) { 22 | this.incidentRepository = incidentRepository; 23 | this.infrastructureComponentRepository = infrastructureComponentRepository; 24 | } 25 | 26 | @GetMapping("/") 27 | public String dashboard(Model model) { 28 | model.addAttribute("inProgress", 29 | this.incidentRepository.findAllByStatusOrderByHappenedOnDesc(IncidentStatus.IN_PROGRESS)); 30 | model.addAttribute("scheduled", 31 | this.incidentRepository.findAllByStatusOrderByHappenedOnDesc(IncidentStatus.SCHEDULED)); 32 | model.addAttribute("resolved", 33 | this.incidentRepository.findByStatusOrderByHappenedOnDesc(IncidentStatus.RESOLVED, Pageable.ofSize(5))); 34 | 35 | model.addAttribute("components", this.infrastructureComponentRepository.findAll(Sort.by("name"))); 36 | return "dashboard"; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/faker/OnlineGaming.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.faker; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDate; 5 | import java.time.LocalDateTime; 6 | import java.time.temporal.ChronoUnit; 7 | import java.util.Locale; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import net.datafaker.providers.base.AbstractProvider; 11 | import net.datafaker.providers.base.BaseProviders; 12 | 13 | import org.springframework.core.io.ClassPathResource; 14 | 15 | class OnlineGaming extends AbstractProvider { 16 | 17 | public OnlineGaming(BaseProviders faker) { 18 | super(faker); 19 | ClassPathResource resource = new ClassPathResource("faker/online-gaming.yml"); 20 | try { 21 | faker.addUrl(Locale.ENGLISH, resource.getURL()); 22 | } 23 | catch (IOException ex) { 24 | throw new IllegalStateException("Could not load custom fake data file", ex); 25 | } 26 | } 27 | 28 | public String maintenance() { 29 | return resolve("incidents.maintenance"); 30 | } 31 | 32 | public String issue() { 33 | return resolve("incidents.issue"); 34 | } 35 | 36 | public String incidentUpdate() { 37 | return resolve("incidents.update"); 38 | } 39 | 40 | public String infrastructureComponent() { 41 | return resolve("incidents.component"); 42 | } 43 | 44 | public LocalDate scheduleDate() { 45 | return LocalDate.parse(this.faker.date().future(10, TimeUnit.DAYS, "YYYY-MM-dd")); 46 | } 47 | 48 | public LocalDate pastIncidentDate() { 49 | return LocalDate.parse(this.faker.date().past(30, TimeUnit.DAYS, "YYYY-MM-dd")); 50 | } 51 | 52 | public LocalDateTime reportItemTimestamp(LocalDate incidentDate) { 53 | return incidentDate.atStartOfDay().plus(this.faker.date().duration(24 * 60, ChronoUnit.MINUTES)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /certs/example-org.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCi4PzSOpATj287 3 | B1LnZff667BtU59DzGrPn7jo1P50pDpHnLUUUWpKjsEo7ErdBFOf9XIx9TlDuHw2 4 | O6ttbivz0wYnyQyIhEDjXPjnuS8gQE3l7AD3P/zirta6PjhyIxjnJqKalF7TFXuL 5 | 94QHBQkGokaNp+XxU8djrIQ6xu8g6WZZmPJTvgXG2Zn55RUAzgEXBY7wzG08+APa 6 | fmYRv3sz2UwuhFDc4Gxh2sidgzwxfVYRviJ6m2wRvShXI+NOpR/HP2kOKrLI3Kyk 7 | 5FUttrZFl8qx7OWcBLH/PhXN01xPfGvgfO9Vx4TRQgdYPoLmVf0rnXYa+c8dq4xd 8 | 5gwSe+zZAgMBAAECggEAOYMiIoVlPFLoXrp+TpDV2DcCzAe78++pQ3jNjQEwfDVF 9 | EuZFllANLRhtIisVYCdX8+JyGSvStZPd4DR/mptNT8ISqVe3Yjj4xI+eoAvmlQe/ 10 | udD0ollozQ4ZahfwTHUSJQSiY9zCAtzSDCEw8F2Zy0rfiMNhUS5Y+FwBMNPvufm/ 11 | tNzGczJjyjJn3DP82ManiIZJ1WpApYxhARH2wJmUs4z0MlKMqY3Y1txfxBhsG4WN 12 | jPnQda58OWppl98eo0iOfW/RH3yjSTU/oNvy834Zq8FZcdOyBQWN4ViRciqWseVR 13 | hbi/ZErObUUbGFfTghiqiiKCaxpcEkF3cuuFpUiq9QKBgQDWlU+TS9nQN3vFGMhp 14 | ydhA4+3tWt6+UaVEn0ryRFit12rhSMdZBUkayoVm1f+0uz5qHlypkGGURy86UrLH 15 | XFleTupfn9F4Pk9e0WzCLsotRmknUQdY/XgLWGhhZOhoJajVsUG/0rVcEb73+e+p 16 | c4Tc6qPdzUld/Mp1Q8WTOT57cwKBgQDCUO5u3JruO5v7D43149R1vwZ67NMIYoL2 17 | e+rqr6Dd1POGmFujsAwztAQ7EOmDXo2s9yNUK6afZ/q91iXgs6KWto5Vwn5S+vE2 18 | WZhMrUrXllSaVqEHdjVyCfvq1GyFSdpGYNroCTbKZOoPXQljjhhq4mlEE7ZnGVFK 19 | YlJhXWP7gwKBgQCYMjDNpWfo4eF8izZiqhIi/EceKWyBCoGw6VaL/OP08SxSe39A 20 | 6ZnPUcNKjBAgjQoY1E4eylQbil25/TvmYN7WIBzmFAHLSk6bTujX1b36XM1qYHNY 21 | r/a7/UmTgrHAZK2aW23p8zZFBiUv1usggdnDovz07YzTB+BFSftRj7F8dwKBgFu3 22 | QWMYQxRRFGViyRmXSI+u80sP/ueFP9VBVfchoGcz/SG5Rf+zt36r6BdM+zrjZTGP 23 | kKBI3iN9O49gxY4Sm5pRXktCOsfF5BZIIaeHX7z0GsiiPO09sSo7ZilHzFT5L8pq 24 | Ksi8mJzdFtDbk/PmfMXuScs1FrIA9CqMz99e6jMrAoGBAJ64QSoOT9ti+TtgH0+q 25 | afeLwKcPoWpRO1UC+WHHlvPjVagUiayXHOrT6O82YDE5Mv8QluSquPGgBv3DFfm7 26 | ETjFLrxgkwHf4QDw5G3R8dsY0VctWuVcTqkzAAthIMkgAElw8lf4JcX1DVHDLlAh 27 | w+zKtYTLEEC+nPDPuHd7Tp0j 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/checker/AvailabilityCheckClient.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.checker; 2 | 3 | import io.spring.sample.opsstatus.availability.Availability; 4 | import io.spring.sample.opsstatus.availability.InfrastructureComponent; 5 | 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.client.RestClient; 10 | import org.springframework.web.client.RestClientException; 11 | 12 | public class AvailabilityCheckClient { 13 | 14 | private final RestClient client; 15 | 16 | public AvailabilityCheckClient(RestClient.Builder builder, String serviceUrl) { 17 | this.client = builder.baseUrl(serviceUrl).build(); 18 | } 19 | 20 | public Availability checkAvailability(InfrastructureComponent component) { 21 | 22 | try { 23 | ResponseEntity response = this.client.get() 24 | .uri(component.getCheckUri()) 25 | .accept(MediaType.APPLICATION_JSON) 26 | .header(HttpHeaders.AUTHORIZATION, "Bearer " + component.getCheckToken()) 27 | .retrieve() 28 | .toEntity(AvailabilityResponse.class); 29 | return getAvailability(response.getBody()); 30 | } 31 | catch (RestClientException ex) { 32 | return Availability.UNAVAILABLE; 33 | } 34 | 35 | } 36 | 37 | private Availability getAvailability(AvailabilityResponse response) { 38 | if (response == null) { 39 | return Availability.UNKNOWN; 40 | } 41 | return response.toAvailability(); 42 | } 43 | 44 | record AvailabilityResponse(String status) { 45 | 46 | private Availability toAvailability() { 47 | if ("OK".equals(status)) { 48 | return Availability.OPERATIONAL; 49 | } 50 | else { 51 | return Availability.UNKNOWN; 52 | } 53 | } 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | restmocks: 3 | image: dotronglong/faker:stable 4 | container_name: restmocks 5 | ports: 6 | - '3030:3030' 7 | volumes: 8 | - ./docker/restmocks:/app/mocks 9 | postgres: 10 | image: postgres:15.6-alpine 11 | container_name: postgres 12 | environment: 13 | - POSTGRES_DB=opsstatus 14 | - POSTGRES_USER=spring 15 | - POSTGRES_PASSWORD=secret 16 | ports: 17 | - '5432:5432' 18 | volumes: 19 | - postgres:/var/lib/postgresql/data 20 | prometheus: 21 | image: prom/prometheus 22 | container_name: prometheus 23 | restart: unless-stopped 24 | extra_hosts: [ 'host.docker.internal:host-gateway' ] 25 | ports: 26 | - '9090:9090' 27 | volumes: 28 | - ./docker/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yaml 29 | - prometheus:/var/lib/prometheus/data 30 | command: 31 | - "--enable-feature=exemplar-storage" 32 | - "--web.enable-remote-write-receiver" 33 | - "--config.file=/etc/prometheus/prometheus.yaml" 34 | grafana: 35 | image: grafana/grafana-oss 36 | container_name: grafana 37 | restart: unless-stopped 38 | extra_hosts: [ 'host.docker.internal:host-gateway' ] 39 | ports: 40 | - '3000:3000' 41 | environment: 42 | - GF_SECURITY_ADMIN_PASSWORD=secret 43 | volumes: 44 | - ./docker/grafana/grafana.ini:/etc/grafana/grafana.ini:ro 45 | - ./docker/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources 46 | - grafana:/var/lib/grafana 47 | zipkin: 48 | image: ghcr.io/openzipkin/zipkin-slim 49 | container_name: zipkin 50 | environment: 51 | - STORAGE_TYPE=mem 52 | ports: 53 | - '9411:9411' 54 | 55 | volumes: 56 | postgres: 57 | driver: local 58 | prometheus: 59 | driver: local 60 | grafana: 61 | driver: local 62 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/dashboard/ApiController.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.dashboard; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import io.spring.sample.opsstatus.incident.Incident; 9 | import io.spring.sample.opsstatus.incident.IncidentOrigin; 10 | import io.spring.sample.opsstatus.incident.IncidentRepository; 11 | import io.spring.sample.opsstatus.incident.IncidentStatus; 12 | import io.spring.sample.opsstatus.incident.ItemType; 13 | import io.spring.sample.opsstatus.incident.ReportItem; 14 | 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.web.bind.annotation.ExceptionHandler; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.ResponseStatus; 21 | import org.springframework.web.bind.annotation.RestController; 22 | import org.springframework.web.servlet.ModelAndView; 23 | 24 | @RestController 25 | @RequestMapping("/api") 26 | public class ApiController { 27 | 28 | private final IncidentRepository repository; 29 | 30 | public ApiController(IncidentRepository repository) { 31 | this.repository = repository; 32 | } 33 | 34 | @GetMapping("/incidents") 35 | public IncidentsDescriptor incidents() { 36 | return new IncidentsDescriptor(this.repository.streamAll().map(IncidentInfo::fromIncident).toList()); 37 | } 38 | 39 | @GetMapping("/incidents/{id}") 40 | public IncidentInfo incident(@PathVariable Long id) { 41 | return this.repository.findById(id) 42 | .map(IncidentInfo::fromIncident) 43 | .orElseThrow(() -> new MissingIncidentException(id)); 44 | } 45 | 46 | @ExceptionHandler(produces = "text/html") 47 | @ResponseStatus(HttpStatus.NOT_FOUND) 48 | public ModelAndView handleHtmlError(MissingIncidentException ex) { 49 | return new ModelAndView("apierror", Map.of("exception", ex)); 50 | } 51 | 52 | public record IncidentsDescriptor(List incidents) { 53 | } 54 | 55 | public record IncidentInfo(Long id, String title, String description, LocalDate happenedOn, IncidentOrigin origin, 56 | IncidentStatus status, List updates) { 57 | 58 | static IncidentInfo fromIncident(Incident incident) { 59 | return new IncidentInfo(incident.getId(), incident.getTitle(), incident.getDescription(), 60 | incident.getHappenedOn(), incident.getOrigin(), incident.getStatus(), 61 | incident.getReportItems().stream().map(IncidentUpdate::fromReportItem).toList()); 62 | } 63 | 64 | } 65 | 66 | public record IncidentUpdate(ItemType type, LocalDateTime date, String description) { 67 | 68 | static IncidentUpdate fromReportItem(ReportItem item) { 69 | return new IncidentUpdate(item.getType(), item.getDate(), item.getDescription()); 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/availability/InfrastructureComponent.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.availability; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | import org.springframework.data.annotation.Id; 9 | 10 | public final class InfrastructureComponent { 11 | 12 | @Id 13 | private Long id; 14 | 15 | private String name; 16 | 17 | private String description; 18 | 19 | private String checkUri; 20 | 21 | private String checkToken; 22 | 23 | private LocalDateTime lastCheckedOn; 24 | 25 | private Availability availability; 26 | 27 | private List outages = new ArrayList<>(); 28 | 29 | public Long getId() { 30 | return this.id; 31 | } 32 | 33 | public void setId(Long id) { 34 | this.id = id; 35 | } 36 | 37 | public String getName() { 38 | return this.name; 39 | } 40 | 41 | public void setName(String name) { 42 | this.name = name; 43 | } 44 | 45 | public String getDescription() { 46 | return this.description; 47 | } 48 | 49 | public void setDescription(String description) { 50 | this.description = description; 51 | } 52 | 53 | public String getCheckUri() { 54 | return this.checkUri; 55 | } 56 | 57 | public void setCheckUri(String checkUri) { 58 | this.checkUri = checkUri; 59 | } 60 | 61 | public String getCheckToken() { 62 | return this.checkToken; 63 | } 64 | 65 | public void setCheckToken(String checkToken) { 66 | this.checkToken = checkToken; 67 | } 68 | 69 | public LocalDateTime getLastCheckedOn() { 70 | return this.lastCheckedOn; 71 | } 72 | 73 | public void setLastCheckedOn(LocalDateTime lastCheckedOn) { 74 | this.lastCheckedOn = lastCheckedOn; 75 | } 76 | 77 | public Availability getAvailability() { 78 | return this.availability; 79 | } 80 | 81 | public void setAvailability(Availability availability) { 82 | this.availability = availability; 83 | } 84 | 85 | public List getOutages() { 86 | return this.outages; 87 | } 88 | 89 | public void setOutages(List outages) { 90 | this.outages = outages; 91 | } 92 | 93 | public void addOutage(Outage outage) { 94 | this.outages.add(outage); 95 | } 96 | 97 | public boolean isOperational() { 98 | return this.availability == Availability.OPERATIONAL; 99 | } 100 | 101 | public boolean isPartiallyAvailable() { 102 | return this.availability == Availability.PARTIAL; 103 | } 104 | 105 | public boolean isUnavailable() { 106 | return this.availability == Availability.UNAVAILABLE; 107 | } 108 | 109 | @Override 110 | public boolean equals(Object o) { 111 | if (this == o) 112 | return true; 113 | if (!(o instanceof InfrastructureComponent that)) 114 | return false; 115 | return Objects.equals(name, that.name); 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | return Objects.hashCode(name); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/incident/Incident.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | import java.time.LocalDate; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.springframework.data.annotation.Id; 8 | 9 | public final class Incident { 10 | 11 | @Id 12 | private Long id; 13 | 14 | private String title; 15 | 16 | private String description; 17 | 18 | private LocalDate happenedOn; 19 | 20 | private IncidentOrigin origin; 21 | 22 | private IncidentStatus status; 23 | 24 | private List reportItems = new ArrayList<>(); 25 | 26 | public static Incident declareIncident(String title, String description) { 27 | Incident incident = new Incident(); 28 | incident.title = title; 29 | incident.description = description; 30 | incident.happenedOn = LocalDate.now(); 31 | incident.origin = IncidentOrigin.ISSUE; 32 | incident.status = IncidentStatus.IN_PROGRESS; 33 | return incident; 34 | } 35 | 36 | public static Incident scheduleMaintenance(String title, String description, LocalDate maintenanceDate) { 37 | Incident incident = new Incident(); 38 | incident.title = title; 39 | incident.happenedOn = LocalDate.now(); 40 | incident.origin = IncidentOrigin.MAINTENANCE; 41 | incident.status = IncidentStatus.SCHEDULED; 42 | return incident; 43 | } 44 | 45 | public Long getId() { 46 | return this.id; 47 | } 48 | 49 | public void setId(Long id) { 50 | this.id = id; 51 | } 52 | 53 | public String getTitle() { 54 | return this.title; 55 | } 56 | 57 | public void setTitle(String title) { 58 | this.title = title; 59 | } 60 | 61 | public String getDescription() { 62 | return this.description; 63 | } 64 | 65 | public void setDescription(String description) { 66 | this.description = description; 67 | } 68 | 69 | public LocalDate getHappenedOn() { 70 | return this.happenedOn; 71 | } 72 | 73 | public void setHappenedOn(LocalDate happenedOn) { 74 | this.happenedOn = happenedOn; 75 | } 76 | 77 | public IncidentOrigin getOrigin() { 78 | return this.origin; 79 | } 80 | 81 | public void setOrigin(IncidentOrigin origin) { 82 | this.origin = origin; 83 | } 84 | 85 | public IncidentStatus getStatus() { 86 | return this.status; 87 | } 88 | 89 | public void setStatus(IncidentStatus status) { 90 | this.status = status; 91 | } 92 | 93 | public List getReportItems() { 94 | return this.reportItems; 95 | } 96 | 97 | public void setReportItems(List reportItems) { 98 | this.reportItems = reportItems; 99 | } 100 | 101 | public void addReportItem(ReportItem reportItem) { 102 | this.reportItems.add(reportItem); 103 | } 104 | 105 | public void resolve() { 106 | this.status = IncidentStatus.RESOLVED; 107 | } 108 | 109 | public boolean isMaintenance() { 110 | return this.origin == IncidentOrigin.MAINTENANCE; 111 | } 112 | 113 | public boolean isIssue() { 114 | return this.origin == IncidentOrigin.ISSUE; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/test/java/io/spring/sample/opsstatus/dashboard/DashboardControllerTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.dashboard; 2 | 3 | import java.time.LocalDate; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import io.spring.sample.opsstatus.availability.InfrastructureComponent; 9 | import io.spring.sample.opsstatus.availability.InfrastructureComponentRepository; 10 | import io.spring.sample.opsstatus.faker.DemoDataGenerator; 11 | import io.spring.sample.opsstatus.incident.Incident; 12 | import io.spring.sample.opsstatus.incident.IncidentRepository; 13 | import io.spring.sample.opsstatus.incident.IncidentStatus; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 18 | import org.springframework.boot.test.mock.mockito.MockBean; 19 | import org.springframework.data.domain.Page; 20 | import org.springframework.data.domain.PageImpl; 21 | import org.springframework.data.domain.Pageable; 22 | import org.springframework.data.domain.Sort; 23 | import org.springframework.test.web.servlet.MockMvc; 24 | 25 | import static org.mockito.Mockito.when; 26 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 29 | 30 | @WebMvcTest(controllers = DashboardController.class) 31 | class DashboardControllerTests { 32 | 33 | @Autowired 34 | private MockMvc mvc; 35 | 36 | @MockBean 37 | private IncidentRepository incidentRepository; 38 | 39 | @MockBean 40 | private InfrastructureComponentRepository infrastructureComponentRepository; 41 | 42 | @Test 43 | void getPopulatesModel() throws Exception { 44 | Incident inProgress = createTestIncident("Oops", "something failed", IncidentStatus.IN_PROGRESS); 45 | when(incidentRepository.findAllByStatusOrderByHappenedOnDesc(IncidentStatus.IN_PROGRESS)) 46 | .thenReturn(List.of(inProgress)); 47 | when(incidentRepository.findAllByStatusOrderByHappenedOnDesc(IncidentStatus.SCHEDULED)) 48 | .thenReturn(Collections.emptyList()); 49 | Page resolvedIncidents = new PageImpl<>( 50 | List.of(createTestIncident("Oopsie", "something went wrong", IncidentStatus.RESOLVED))); 51 | when(incidentRepository.findByStatusOrderByHappenedOnDesc(IncidentStatus.RESOLVED, Pageable.ofSize(5))) 52 | .thenReturn(resolvedIncidents); 53 | Set infrastructureComponents = new DemoDataGenerator().infrastructureComponents(10); 54 | when(infrastructureComponentRepository.findAll(Sort.by("name"))).thenReturn(infrastructureComponents); 55 | mvc.perform(get("/")) 56 | .andExpect(view().name("dashboard")) 57 | .andExpect(model().attribute("inProgress", List.of(inProgress))) 58 | .andExpect(model().attribute("scheduled", List.of())) 59 | .andExpect(model().attribute("resolved", resolvedIncidents)) 60 | .andExpect(model().attribute("components", infrastructureComponents)); 61 | } 62 | 63 | private Incident createTestIncident(String title, String description, IncidentStatus status) { 64 | Incident incident = new Incident(); 65 | incident.setTitle(title); 66 | incident.setDescription(description); 67 | incident.setHappenedOn(LocalDate.now()); 68 | incident.setStatus(status); 69 | return incident; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/io/spring/sample/opsstatus/faker/DemoDataGenerator.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.faker; 2 | 3 | import java.time.LocalDate; 4 | import java.util.ArrayList; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import io.spring.sample.opsstatus.availability.Availability; 10 | import io.spring.sample.opsstatus.availability.InfrastructureComponent; 11 | import io.spring.sample.opsstatus.incident.Incident; 12 | import io.spring.sample.opsstatus.incident.IncidentOrigin; 13 | import io.spring.sample.opsstatus.incident.IncidentStatus; 14 | import io.spring.sample.opsstatus.incident.ItemType; 15 | import io.spring.sample.opsstatus.incident.ReportItem; 16 | 17 | import org.springframework.util.StringUtils; 18 | import org.springframework.web.util.UriComponentsBuilder; 19 | 20 | public class DemoDataGenerator { 21 | 22 | private final DataFaker faker = new DataFaker(); 23 | 24 | public List generatePastIncidents(int count) { 25 | List incidents = new ArrayList<>(); 26 | for (int i = 0; i < count; i++) { 27 | LocalDate pastIncidentDate = this.faker.onlineGaming().pastIncidentDate(); 28 | Incident incident = new Incident(); 29 | String[] split = this.faker.onlineGaming().issue().split(" \\| "); 30 | incident.setTitle(split[0]); 31 | incident.setDescription(split[1]); 32 | incident.setStatus(IncidentStatus.RESOLVED); 33 | incident.setHappenedOn(pastIncidentDate); 34 | incident.setOrigin(IncidentOrigin.ISSUE); 35 | ReportItem update = ReportItem.create(ItemType.INVESTIGATING, this.faker.onlineGaming().incidentUpdate()); 36 | update.setDate(this.faker.onlineGaming().reportItemTimestamp(pastIncidentDate)); 37 | incident.addReportItem(update); 38 | ReportItem resolution = ReportItem.create(ItemType.RESOLVED, this.faker.onlineGaming().incidentUpdate()); 39 | update.setDate(this.faker.onlineGaming().reportItemTimestamp(pastIncidentDate)); 40 | incident.addReportItem(resolution); 41 | incidents.add(incident); 42 | } 43 | return incidents; 44 | } 45 | 46 | public Incident generateMaintenanceUpdate() { 47 | LocalDate scheduleDate = this.faker.onlineGaming().scheduleDate(); 48 | Incident incident = new Incident(); 49 | String[] split = this.faker.onlineGaming().maintenance().split(" \\| "); 50 | incident.setTitle(split[0]); 51 | incident.setDescription(split[1]); 52 | incident.setStatus(IncidentStatus.SCHEDULED); 53 | incident.setOrigin(IncidentOrigin.MAINTENANCE); 54 | incident.setHappenedOn(scheduleDate); 55 | return incident; 56 | } 57 | 58 | public Set infrastructureComponents(int count) { 59 | Set components = new HashSet<>(); 60 | while (components.size() < count) { 61 | InfrastructureComponent component = new InfrastructureComponent(); 62 | String[] split = this.faker.onlineGaming().infrastructureComponent().split(" \\| "); 63 | component.setName(split[0]); 64 | component.setDescription(split[1]); 65 | component.setAvailability(Availability.OPERATIONAL); 66 | 67 | String availability = (components.size() != 2) ? "available" : "broken"; 68 | 69 | String checkUrl = UriComponentsBuilder.fromPath("/{name}/{availability}") 70 | .build(toSlug(component.getName()), availability) 71 | .toString(); 72 | component.setCheckUri(checkUrl); 73 | component.setCheckToken(this.faker.internet().password()); 74 | 75 | components.add(component); 76 | } 77 | return components; 78 | } 79 | 80 | private String toSlug(String name) { 81 | String cleaned = name.toLowerCase().replace("\n", " ").replaceAll("[^a-z\\d\\s]", " "); 82 | return StringUtils.arrayToDelimitedString(StringUtils.tokenizeToStringArray(cleaned, " "), "-"); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/io/spring/sample/opsstatus/availability/InfrastructureComponentRepositoryTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.availability; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import io.spring.sample.opsstatus.faker.DemoDataGenerator; 6 | import org.junit.jupiter.api.Test; 7 | import org.testcontainers.containers.PostgreSQLContainer; 8 | import org.testcontainers.junit.jupiter.Container; 9 | import org.testcontainers.junit.jupiter.Testcontainers; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; 13 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 14 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; 15 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | @DataJdbcTest 20 | @AutoConfigureTestDatabase(replace = Replace.NONE) 21 | @Testcontainers(disabledWithoutDocker = true) 22 | class InfrastructureComponentRepositoryTests { 23 | 24 | @Container 25 | @ServiceConnection 26 | static final PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15.6-alpine"); 27 | 28 | private final InfrastructureComponentRepository repository; 29 | 30 | private final DemoDataGenerator demoDataGenerator; 31 | 32 | InfrastructureComponentRepositoryTests(@Autowired InfrastructureComponentRepository repository) { 33 | this.repository = repository; 34 | this.demoDataGenerator = new DemoDataGenerator(); 35 | } 36 | 37 | @Test 38 | void createInfrastructureComponent() { 39 | InfrastructureComponent ic = createTestInfrastructureComponent(); 40 | assertThat(ic.getId()).isNull(); 41 | InfrastructureComponent savedIc = this.repository.save(ic); 42 | assertThat(savedIc.getId()).isNotNull(); 43 | } 44 | 45 | @Test 46 | void addUnavailableOutage() { 47 | InfrastructureComponent ic = createTestInfrastructureComponent(); 48 | ic.addOutage(startOutage(Availability.UNAVAILABLE)); 49 | ic.setAvailability(Availability.UNAVAILABLE); 50 | InfrastructureComponent savedIc = this.repository.save(ic); 51 | assertThat(savedIc.isUnavailable()).isTrue(); 52 | assertThat(this.repository.findById(savedIc.getId())) 53 | .hasValueSatisfying(component -> assertThat(component.getOutages()).singleElement().satisfies(outage -> { 54 | assertThat(outage.getAvailability()).isEqualTo(Availability.UNAVAILABLE); 55 | assertThat(outage.getEndedOn()).isNull(); 56 | })); 57 | } 58 | 59 | @Test 60 | void addPartialOutage() { 61 | InfrastructureComponent ic = createTestInfrastructureComponent(); 62 | ic.addOutage(startOutage(Availability.PARTIAL)); 63 | ic.setAvailability(Availability.PARTIAL); 64 | InfrastructureComponent savedIc = this.repository.save(ic); 65 | assertThat(savedIc.isPartiallyAvailable()).isTrue(); 66 | } 67 | 68 | @Test 69 | void resolveOutage() { 70 | InfrastructureComponent ic = createTestInfrastructureComponent(); 71 | ic.addOutage(startOutage(Availability.UNAVAILABLE)); 72 | ic.setAvailability(Availability.UNAVAILABLE); 73 | InfrastructureComponent savedIc = this.repository.save(ic); 74 | Outage outage = savedIc.getOutages().get(0); 75 | outage.setEndedOn(LocalDateTime.now().plusHours(30)); 76 | outage.setAvailability(Availability.OPERATIONAL); 77 | ic.setAvailability(Availability.OPERATIONAL); 78 | InfrastructureComponent finalIc = this.repository.save(savedIc); 79 | assertThat(finalIc.isOperational()).isTrue(); 80 | } 81 | 82 | private InfrastructureComponent createTestInfrastructureComponent() { 83 | return demoDataGenerator.infrastructureComponents(1).iterator().next(); 84 | } 85 | 86 | private Outage startOutage(Availability availability) { 87 | Outage outage = new Outage(); 88 | outage.setAvailability(availability); 89 | outage.setStartedOn(LocalDateTime.now()); 90 | return outage; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/io/spring/sample/opsstatus/checker/AvailabilityCheckClientTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.checker; 2 | 3 | import java.util.List; 4 | 5 | import io.spring.sample.opsstatus.availability.Availability; 6 | import io.spring.sample.opsstatus.availability.InfrastructureComponent; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.TestInstance; 9 | import org.junit.jupiter.api.TestInstance.Lifecycle; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.web.client.MockRestServiceServer; 17 | import org.springframework.test.web.client.RequestMatcher; 18 | import org.springframework.web.client.RestClient; 19 | 20 | import static java.util.Map.entry; 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; 23 | import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; 24 | 25 | @RestClientTest 26 | @TestInstance(Lifecycle.PER_CLASS) 27 | class AvailabilityCheckClientTests { 28 | 29 | private static final String RESPONSE_OK = """ 30 | { "status": "OK" } 31 | """; 32 | 33 | private static final String RESPONSE_UNEXPECTED = """ 34 | { "name": "unexpected" } 35 | """; 36 | 37 | private static final String RESPONSE_DOWN = """ 38 | { "status": "DOWN" } 39 | """; 40 | 41 | private final AvailabilityCheckClient client; 42 | 43 | private final MockRestServiceServer server; 44 | 45 | AvailabilityCheckClientTests(@Autowired RestClient.Builder restClientBuilder, 46 | @Autowired MockRestServiceServer server) { 47 | this.client = new AvailabilityCheckClient(restClientBuilder, "https://example.com"); 48 | this.server = server; 49 | } 50 | 51 | @Test 52 | void checkAvailabilitySuccess() { 53 | InfrastructureComponent component = new InfrastructureComponent(); 54 | component.setCheckUri("/test/availability"); 55 | component.setCheckToken("test-123"); 56 | this.server.expect(prepareRequest("/test/availability", "test-123")) 57 | .andRespond(withSuccess(RESPONSE_OK, MediaType.APPLICATION_JSON)); 58 | assertThat(client.checkAvailability(component)).isEqualTo(Availability.OPERATIONAL); 59 | } 60 | 61 | @Test 62 | void checkAvailabilitySuccessWithUnexpectedStatus() { 63 | InfrastructureComponent component = new InfrastructureComponent(); 64 | component.setCheckUri("/test/availability"); 65 | component.setCheckToken("test-123"); 66 | this.server.expect(prepareRequest("/test/availability", "test-123")) 67 | .andRespond(withSuccess(RESPONSE_UNEXPECTED, MediaType.APPLICATION_JSON)); 68 | assertThat(client.checkAvailability(component)).isEqualTo(Availability.UNKNOWN); 69 | } 70 | 71 | @Test 72 | void checkAvailabilitySuccessWithNoStatus() { 73 | InfrastructureComponent component = new InfrastructureComponent(); 74 | component.setCheckUri("/test/availability"); 75 | component.setCheckToken("test-123"); 76 | this.server.expect(prepareRequest("/test/availability", "test-123")).andRespond(withStatus(HttpStatus.OK)); 77 | assertThat(client.checkAvailability(component)).isEqualTo(Availability.UNKNOWN); 78 | } 79 | 80 | @Test 81 | void checkAvailabilityFailure() { 82 | InfrastructureComponent component = new InfrastructureComponent(); 83 | component.setCheckUri("/broken/availability"); 84 | component.setCheckToken("test-123"); 85 | this.server.expect(prepareRequest("/broken/availability", "test-123")) 86 | .andRespond(withStatus(HttpStatus.SERVICE_UNAVAILABLE).body(RESPONSE_DOWN)); 87 | assertThat(client.checkAvailability(component)).isEqualTo(Availability.UNAVAILABLE); 88 | } 89 | 90 | public static RequestMatcher prepareRequest(String path, String token) { 91 | return (request) -> { 92 | assertThat(request.getURI()).hasPath(path); 93 | assertThat(request.getHeaders()).contains( 94 | entry(HttpHeaders.ACCEPT, List.of(MediaType.APPLICATION_JSON_VALUE)), 95 | entry(HttpHeaders.AUTHORIZATION, List.of("Bearer %s".formatted(token)))); 96 | }; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/io/spring/sample/opsstatus/incident/IncidentRepositoryTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.incident; 2 | 3 | import java.time.LocalDate; 4 | import java.util.List; 5 | 6 | import io.spring.sample.opsstatus.faker.DemoDataGenerator; 7 | import org.junit.jupiter.api.Test; 8 | import org.testcontainers.containers.PostgreSQLContainer; 9 | import org.testcontainers.junit.jupiter.Container; 10 | import org.testcontainers.junit.jupiter.Testcontainers; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; 14 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 15 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; 16 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 17 | import org.springframework.data.domain.Pageable; 18 | import org.springframework.data.domain.Slice; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | 22 | @DataJdbcTest 23 | @AutoConfigureTestDatabase(replace = Replace.NONE) 24 | @Testcontainers(disabledWithoutDocker = true) 25 | class IncidentRepositoryTests { 26 | 27 | @Container 28 | @ServiceConnection 29 | static final PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15.6-alpine"); 30 | 31 | private final IncidentRepository repository; 32 | 33 | private final DemoDataGenerator demoDataGenerator; 34 | 35 | IncidentRepositoryTests(@Autowired IncidentRepository repository) { 36 | this.repository = repository; 37 | this.demoDataGenerator = new DemoDataGenerator(); 38 | } 39 | 40 | @Test 41 | void createIncident() { 42 | List incidents = this.demoDataGenerator.generatePastIncidents(5); 43 | assertThat(incidents).allSatisfy(ic -> assertThat(ic.getId()).isNull()); 44 | this.repository.saveAll(incidents); 45 | assertThat(incidents).allSatisfy(ic -> assertThat(ic.getId()).isNotNull()); 46 | } 47 | 48 | @Test 49 | void streamAllSortIncidentByHappenedOnField() { 50 | List incidents = List.of(createIncident(LocalDate.of(2024, 2, 7), IncidentStatus.RESOLVED), 51 | createIncident(LocalDate.of(2024, 2, 25), IncidentStatus.IN_PROGRESS), 52 | createIncident(LocalDate.of(2024, 2, 9), IncidentStatus.RESOLVED), 53 | createIncident(LocalDate.of(2024, 2, 17), IncidentStatus.RESOLVED)); 54 | this.repository.saveAll(incidents); 55 | assertThat(this.repository.streamAll()).extracting(Incident::getHappenedOn) 56 | .containsExactly(LocalDate.of(2024, 2, 25), LocalDate.of(2024, 2, 17), LocalDate.of(2024, 2, 9), 57 | LocalDate.of(2024, 2, 7)); 58 | } 59 | 60 | @Test 61 | void findAllByStatusOrderByHappenedOnDesc() { 62 | List incidents = List.of(createIncident(LocalDate.of(2024, 2, 7), IncidentStatus.RESOLVED), 63 | createIncident(LocalDate.of(2024, 2, 25), IncidentStatus.IN_PROGRESS), 64 | createIncident(LocalDate.of(2024, 2, 9), IncidentStatus.RESOLVED), 65 | createIncident(LocalDate.of(2024, 2, 17), IncidentStatus.RESOLVED)); 66 | this.repository.saveAll(incidents); 67 | assertThat(this.repository.findAllByStatusOrderByHappenedOnDesc(IncidentStatus.RESOLVED)) 68 | .extracting(Incident::getHappenedOn) 69 | .containsExactly(LocalDate.of(2024, 2, 17), LocalDate.of(2024, 2, 9), LocalDate.of(2024, 2, 7)); 70 | } 71 | 72 | @Test 73 | void findByStatusOrderByHappenedOnDesc() { 74 | List incidents = List.of(createIncident(LocalDate.of(2024, 2, 7), IncidentStatus.RESOLVED), 75 | createIncident(LocalDate.of(2024, 2, 25), IncidentStatus.IN_PROGRESS), 76 | createIncident(LocalDate.of(2024, 2, 9), IncidentStatus.RESOLVED), 77 | createIncident(LocalDate.of(2024, 2, 17), IncidentStatus.RESOLVED)); 78 | this.repository.saveAll(incidents); 79 | Slice slice = this.repository.findByStatusOrderByHappenedOnDesc(IncidentStatus.RESOLVED, 80 | Pageable.ofSize(2)); 81 | assertThat(slice.getSize()).isEqualTo(2); 82 | assertThat(slice.hasNext()).isTrue(); 83 | assertThat(slice.getContent()).extracting(Incident::getHappenedOn) 84 | .containsExactly(LocalDate.of(2024, 2, 17), LocalDate.of(2024, 2, 9)); 85 | } 86 | 87 | private Incident createIncident(LocalDate happenedOn, IncidentStatus status) { 88 | Incident incident = new Incident(); 89 | incident.setTitle("oops"); 90 | incident.setDescription("oops oops"); 91 | incident.setHappenedOn(happenedOn); 92 | incident.setStatus(status); 93 | return incident; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/resources/templates/dashboard.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Online Gaming - Status page 7 | 8 | 9 | 10 | 11 |
12 | 16 |
17 |
18 |
19 |

#Infrastructure

20 |

Current Status of our online platform.

21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{#components}} 32 | 33 | 34 | 38 | 39 | 40 | {{/components}} 41 | 42 |
ComponentAvailabilityLast checked
{{name}} 35 | {{availability}} 36 | 37 | {{#lastCheckedOn}}{{.}}{{/lastCheckedOn}}{{^lastCheckedOn}}N/A{{/lastCheckedOn}}
43 |
44 |
45 |
46 |

#Incidents

47 |
48 |

In progress

49 | {{#inProgress}} 50 |

{{title}}

51 | Created on {{happenedOn}} 52 |

{{description}}

53 | {{/inProgress}} 54 | {{^inProgress}} 55 | No incident! 56 | {{/inProgress}} 57 |
58 |
59 |

Scheduled Maintenance

60 | {{#scheduled}} 61 |
62 |

{{title}}

63 | on {{happenedOn}} 64 |

{{description}}

65 | Details 66 |
67 | {{/scheduled}} 68 |
69 |
70 |

Past Incidents

71 | {{#resolved}} 72 |
73 |

{{title}}

74 | on {{happenedOn}} 75 |

{{description}}

76 | Details 77 |
78 | {{/resolved}} 79 |
80 |
81 |
82 |
83 |
84 |
85 | 86 |
87 |

Hello, is there a REST API for incidents?

88 |
89 |
90 | 91 |
92 |
93 |

Yes, right here: /api/incidents!

94 |
95 | 96 |
97 |
98 |
99 | 100 |
101 | 102 | 105 |
106 | 107 | 112 | 113 | -------------------------------------------------------------------------------- /src/test/java/io/spring/sample/opsstatus/dashboard/ApiControllerTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.sample.opsstatus.dashboard; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.charset.StandardCharsets; 6 | import java.time.LocalDate; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.stream.Stream; 10 | 11 | import io.spring.sample.opsstatus.incident.Incident; 12 | import io.spring.sample.opsstatus.incident.IncidentOrigin; 13 | import io.spring.sample.opsstatus.incident.IncidentRepository; 14 | import io.spring.sample.opsstatus.incident.IncidentStatus; 15 | import org.hamcrest.Matcher; 16 | import org.hamcrest.Matchers; 17 | import org.junit.jupiter.api.Test; 18 | 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 21 | import org.springframework.boot.test.mock.mockito.MockBean; 22 | import org.springframework.core.io.ClassPathResource; 23 | import org.springframework.core.io.Resource; 24 | import org.springframework.http.HttpStatus; 25 | import org.springframework.http.MediaType; 26 | import org.springframework.test.web.servlet.MockMvc; 27 | import org.springframework.util.StreamUtils; 28 | 29 | import static org.hamcrest.collection.IsMapContaining.hasEntry; 30 | import static org.mockito.Mockito.when; 31 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 32 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 35 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 36 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 37 | 38 | @WebMvcTest(controllers = ApiController.class) 39 | class ApiControllerTests { 40 | 41 | @Autowired 42 | private MockMvc mvc; 43 | 44 | @MockBean 45 | private IncidentRepository incidentRepository; 46 | 47 | @Test 48 | void incidentsWhenNoIncident() throws Exception { 49 | when(this.incidentRepository.streamAll()).thenReturn(Stream.empty()); 50 | mvc.perform(get("/api/incidents")).andExpect(status().isOk()) 51 | .andExpect(jsonPath("incidents").isArray()).andExpect(jsonPath("incidents").isEmpty()); 52 | } 53 | 54 | @Test 55 | void incidentsWithIncidents() throws Exception { 56 | List incidents = List.of( 57 | createTestIncident("test", "a description", LocalDate.of(2024, 1, 2), IncidentOrigin.MAINTENANCE, 58 | IncidentStatus.RESOLVED), 59 | createTestIncident("another", "another description", LocalDate.of(2024, 1, 20), IncidentOrigin.ISSUE, 60 | IncidentStatus.IN_PROGRESS)); 61 | when(this.incidentRepository.streamAll()).thenReturn(incidents.stream()); 62 | mvc.perform(get("/api/incidents")) 63 | .andExpect(status().isOk()) 64 | .andExpect(jsonPath("incidents").isArray()) 65 | .andExpect(jsonPath("incidents[0]").value(jsonIncidentMatcher(incidents.get(0)))) 66 | .andExpect(jsonPath("incidents[1]").value(jsonIncidentMatcher(incidents.get(1)))); 67 | } 68 | 69 | @Test 70 | void incident() throws Exception { 71 | Incident testIncident = createTestIncident("test", "a description", LocalDate.of(2024, 1, 2), 72 | IncidentOrigin.MAINTENANCE, IncidentStatus.RESOLVED); 73 | when(this.incidentRepository.findById(42L)).thenReturn(Optional.of(testIncident)); 74 | mvc.perform(get("/api/incidents/42")) 75 | .andExpect(status().isOk()) 76 | .andExpect(jsonPath("$").value(jsonIncidentMatcher(testIncident))); 77 | } 78 | 79 | @Test 80 | void incidentWithNoSuchIncident() throws Exception { 81 | when(this.incidentRepository.findById(42L)).thenReturn(Optional.empty()); 82 | mvc.perform(get("/api/incidents/42").accept(MediaType.APPLICATION_PROBLEM_JSON)) 83 | .andExpect(status().is(HttpStatus.NOT_FOUND.value())) 84 | .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)) 85 | .andExpect(content().json(readJson(new ClassPathResource("missing-incident-42.json", getClass())),true)); 86 | } 87 | 88 | @Test 89 | void incidentWithNoSuchIncidentErrorView() throws Exception { 90 | when(this.incidentRepository.findById(42L)).thenReturn(Optional.empty()); 91 | mvc.perform(get("/api/incidents/42").accept(MediaType.TEXT_HTML)) 92 | .andExpect(status().is(HttpStatus.NOT_FOUND.value())) 93 | .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) 94 | .andExpect(view().name("apierror")) 95 | .andExpect(model().attributeExists("exception")); 96 | } 97 | 98 | private Matcher jsonIncidentMatcher(Incident incident) { 99 | return Matchers.allOf(hasEntry("title", incident.getTitle()), 100 | hasEntry("description", incident.getDescription()), 101 | hasEntry("happenedOn", incident.getHappenedOn().toString()), 102 | hasEntry("origin", incident.getOrigin().toString()), 103 | hasEntry("status", incident.getStatus().toString())); 104 | } 105 | 106 | private String readJson(Resource resource) { 107 | try { 108 | try (InputStream in = resource.getInputStream()) { 109 | return StreamUtils.copyToString(in, StandardCharsets.UTF_8); 110 | } 111 | } 112 | catch (IOException ex) { 113 | throw new IllegalStateException(ex); 114 | } 115 | } 116 | 117 | private Incident createTestIncident(String title, String description, LocalDate happenedOn, IncidentOrigin origin, 118 | IncidentStatus status) { 119 | Incident incident = new Incident(); 120 | incident.setTitle(title); 121 | incident.setDescription(description); 122 | incident.setHappenedOn(happenedOn); 123 | incident.setOrigin(origin); 124 | incident.setStatus(status); 125 | return incident; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/main/resources/faker/online-gaming.yml: -------------------------------------------------------------------------------- 1 | en: 2 | faker: 3 | incidents: 4 | maintenance: [ 5 | "Galactic Conquest Expansion | Unveils an interstellar setting, introducing new planets, space battles, and a galactic conflict storyline.", 6 | "Legends Unleashed Update | Adds legendary characters from the game's lore as playable heroes, each with unique abilities and playstyles.", 7 | "Season of Shadows Event | Launches a mysterious event with shadow-themed challenges, exclusive dark-themed cosmetics, and a shadowy atmosphere.", 8 | "Artifact Ascension Patch | Introduces a system for upgrading and evolving in-game artifacts, providing players with new strategic options.", 9 | "City of Wonders Expansion | Expands the game world with a sprawling city, offering new quests, activities, and a dynamic urban environment.", 10 | "Time-Limited Raids Update | Introduces special raid events with unique challenges and powerful rewards, available for a limited time.", 11 | "Techno-Magic Fusion DLC | Combines technology and magic, introducing hybrid classes, weapons, and abilities that blend the two elements.", 12 | "Mythic Creatures Hunt Event | Unleashes rare mythical creatures into the game world for a limited time, challenging players to hunt and tame them.", 13 | "Dynamic Weather Patch | Implements dynamic weather conditions, affecting gameplay with rain, snow, or storms that impact visibility and tactics.", 14 | "Alliance Wars Update | Introduces a robust alliance system, allowing player groups to engage in strategic wars, capture territories, and earn exclusive rewards.", 15 | "Epic Odyssey | Embark on a grand adventure with new quests, mythical creatures, and legendary loot.", 16 | "Stealth Revolution | Introduces stealth mechanics, allowing players to sneak, assassinate, and outwit opponents.", 17 | "Season of the Phoenix | Rise from the ashes with fiery challenges, phoenix-themed rewards, and a burning battleground.", 18 | "Cybernetic Onslaught | Unleashes a futuristic onslaught with cybernetic enhancements, high-tech weapons, and neon-infused battlegrounds.", 19 | "Mystic Enchantments | Delve into the arcane arts with new spells, magical items, and a mystical realm to explore.", 20 | "Underworld Uprising | Descend into the underworld, facing dark forces, uncovering secrets, and earning shadowy rewards.", 21 | "Aerial Assault | Take the battle to the skies with aerial combat, airships, and floating arenas for intense dogfights.", 22 | "Tech Nexus Overdrive | Boosts the technological side of the game with advanced gadgets, drones, and augmented reality challenges.", 23 | "Time Warp Chronicles | Manipulate time for strategic advantages, solve temporal puzzles, and explore time-bending landscapes.", 24 | "Elemental Mayhem | Introduces elemental chaos with dynamic weather, environmental hazards, and elemental-themed battles." 25 | ] 26 | issue: [ 27 | "Server Downtime | Unplanned outages or scheduled maintenance that temporarily takes the game offline.", 28 | "Lag and Latency | Delays in data transmission causing players to experience lag, affecting real-time gameplay.", 29 | "Connection Drops | Players getting disconnected from the game server, disrupting their gaming sessions.", 30 | "Matchmaking Issues | Problems with pairing players together based on skill level, leading to uneven or unfair matches.", 31 | "Game Crashes | Sudden closures of the game client due to software bugs or compatibility issues.", 32 | "Cheating and Exploits | Players using hacks or exploiting vulnerabilities to gain an unfair advantage, requiring constant monitoring and countermeasures.", 33 | "Balancing Problems | Imbalances in character classes, weapons, or game mechanics affecting the overall fairness of the gameplay.", 34 | "Server Performance | Issues related to the game server's capacity, leading to performance degradation during high player traffic.", 35 | "Patch Problems | Errors or complications arising from the implementation of game patches, causing unintended consequences.", 36 | "Communication Breakdown | Problems with in-game chat, voice communication, or social features that hinder player interaction.", 37 | "Server Overload | Too many players causing strain on servers, leading to performance issues..", 38 | "DDoS Attacks | Deliberate attacks overwhelming servers with traffic, disrupting gameplay..", 39 | "Authentication Failures | Issues with player login and authentication processes..", 40 | "Inventory or Progression Loss | Players experiencing loss of in-game items, progress, or achievements..", 41 | "Exploitable Glitches | Game-breaking bugs that can be exploited for unintended advantages..", 42 | "Chat and Communication Outages | Problems with in-game chat, voice communication, or messaging systems.", 43 | "Unstable Matchmaking | Difficulties in forming balanced matches based on player skills.", 44 | "Economic Imbalances | Issues with in-game economies leading to inflation or unfair advantages.", 45 | "Unresponsive Controls | Input lag or unresponsive controls affecting player actions in real-time.", 46 | "Platform-Specific Problems | Challenges related to different platforms (PC, console) or cross-platform compatibility.", 47 | "Inconsistent Hit Detection | Problems with the accuracy of hit registration in combat scenarios.", 48 | "Payment Processing Issues | Difficulties with processing in-game purchases or subscription fees.", 49 | "Localization Problems | Translation or cultural adaptation issues impacting global player experiences.", 50 | "Banned Player Re-entry | Expelled players finding ways to re-enter the game undetected.", 51 | "Server Crashes | Sudden server failures leading to widespread player disconnections.", 52 | "Geographical Latency | Players experiencing high ping due to server location or network routing issues.", 53 | "Patch Rollback Necessity | Reverting to a previous version due to unforeseen issues with a new update.", 54 | "Inadequate Anti-Cheat Measures | Failing to address cheating effectively, allowing unfair play.", 55 | "Incomplete Feature Implementation | Promised features missing or not working as intended.", 56 | "Unoptimized Game Performance | Poor performance on certain hardware configurations.", 57 | "Unauthorized Access | Hacks or breaches leading to unauthorized access to player accounts.", 58 | "Server Desync | Inconsistencies in game state between clients and servers.", 59 | "Persistent Audio Issues | Problems with in-game sound, affecting immersion and gameplay.", 60 | "Unexpected Downtime | Unplanned server outages causing disruption during peak playtimes.", 61 | "Compatibility with Hardware/Software Updates | Issues arising from players updating their hardware or software.", 62 | "Social Features Dysfunction | Problems with guilds, clans, or group-related in-game social features.", 63 | "Unbalanced PvP Mechanics | Discrepancies in player-versus-player combat leading to dissatisfaction.", 64 | "Unreliable Cloud Services | Dependence on third-party cloud services causing outages.", 65 | "Resource Exploitation | Players finding ways to exploit resources, disrupting the game economy.", 66 | "Community Management Challenges | Handling toxic behavior, addressing player grievances, and maintaining a positive community environment." 67 | ] 68 | update: [ 69 | "Players experiencing intermittent connection issues. Investigating root cause.", 70 | "Root cause traced to a network provider outage. Engaging with the provider for resolution.", 71 | "Implemented temporary measures to stabilize connections. Players may still experience delays.", 72 | "Reports of account authentication failures. Investigating potential security breach.", 73 | "Network provider restored services. Monitoring for any lingering issues. Compensation plan under consideration.", 74 | "Security breach identified. Implementing enhanced security measures. Resetting affected accounts.", 75 | "Security measures successfully implemented. All affected accounts secured. Further investigations ongoing.", 76 | "Reports of server crashes. Investigating to identify the root cause.", 77 | "Root cause identified as a memory leak issue. Preparing a hotfix for immediate deployment.", 78 | "Hotfix deployed successfully. Server stability restored. Monitoring for any residual issues.", 79 | "Players reporting login failures. Investigating potential server authentication issues.", 80 | "Authentication server overload causing login failures. Scaling resources to handle increased demand.", 81 | "Deployed additional authentication servers to handle the load. Login success rates improving.", 82 | "Stabilized authentication servers. All login issues resolved. Implementing preventive measures.", 83 | "Reports of in-game purchases not reflecting. Investigating transaction processing issues.", 84 | "Issue traced to a payment gateway disruption. Engaging with the payment service provider for resolution.", 85 | "Implemented a temporary workaround for in-game purchases. Players can now make transactions with some delay.", 86 | "Payment gateway issues resolved. In-game purchases functioning normally. Monitoring for any residual effects.", 87 | "Server instability reported, leading to sporadic disconnections. Investigating root cause.", 88 | "Identified a network provider issue affecting server connectivity. Engaging with the provider for a fix.", 89 | "Network provider restored services. Server connectivity stabilized. Implementing measures to prevent recurrence." 90 | ] 91 | component: [ 92 | "Game Servers | Responsible for hosting game sessions, managing player interactions, and processing game logic.", 93 | "Database Cluster | Stores and manages player profiles, game state, and other persistent data.", 94 | "Load Balancer | Distributes incoming player traffic across multiple game servers to ensure even load distribution.", 95 | "Content Delivery Network (CDN) | Optimizes the delivery of game assets, reducing latency and enhancing player experience.", 96 | "Authentication Service | Verifies player identities, manages logins, and ensures secure access to the game.", 97 | "Monitoring and Analytics | Collects and analyzes data on player behavior, server performance, and system health.", 98 | "Payment Gateway | Handles in-game transactions, purchases, and manages virtual currency exchanges securely.", 99 | "Networking Infrastructure | Provides the underlying framework for player connections, ensuring low latency and high reliability.", 100 | "Content Management System (CMS) | Manages and deploys game updates, patches, and new content to players.", 101 | "Security Layer | Implements measures to protect against DDoS attacks, cheating, and unauthorized access." 102 | ] --------------------------------------------------------------------------------