Skip to content

Commit

Permalink
Merge pull request #612 from NUM-Forschungsdatenplattform/feature/use…
Browse files Browse the repository at this point in the history
…rs-metrics

Feature/users metrics
  • Loading branch information
ramueSVA authored Sep 13, 2024
2 parents 2dd9e49 + 8b1bf0a commit 40bc1c1
Show file tree
Hide file tree
Showing 19 changed files with 537 additions and 351 deletions.
20 changes: 13 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
<skip.unit.tests>false</skip.unit.tests>
<postgresql.version>42.7.4</postgresql.version>
<hapi.version>6.8.3</hapi.version>
<wiremock-standalone.version>3.9.1</wiremock-standalone.version>
<testcontainers.version>1.20.1</testcontainers.version>
<mockserver-client.version>5.15.0</mockserver-client.version>
<ossindexAnalyzerEnabled>false</ossindexAnalyzerEnabled>
<spring.cloud-version>2023.0.3</spring.cloud-version>
</properties>
Expand Down Expand Up @@ -142,6 +142,18 @@
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mockserver</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java</artifactId>
<version>${mockserver-client.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
Expand Down Expand Up @@ -248,12 +260,6 @@
<version>3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock-standalone.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Arrays;

import static org.highmed.numportal.service.UserService.TRANSLATION_CACHE;
import static org.highmed.numportal.service.UserService.USERS_CACHE;

@Configuration
@EnableCaching
Expand All @@ -21,7 +22,7 @@ public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();

Cache aqlParametersCache = new ConcurrentMapCache("aqlParameters", false);
Cache usersCache = new ConcurrentMapCache("users", false);
Cache usersCache = new ConcurrentMapCache(USERS_CACHE, false);
Cache translationsCache = new ConcurrentMapCache(TRANSLATION_CACHE, false);

cacheManager.setCaches(Arrays.asList(aqlParametersCache, usersCache, translationsCache));
Expand Down
27 changes: 16 additions & 11 deletions src/main/java/org/highmed/numportal/service/UserDetailsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.highmed.numportal.service.exception.ForbiddenException;
import org.highmed.numportal.service.exception.ResourceNotFound;
import org.highmed.numportal.service.exception.SystemException;
import org.highmed.numportal.service.metric.UsersMetrics;
import org.highmed.numportal.service.notification.NotificationService;
import org.highmed.numportal.service.notification.dto.NewUserNotification;
import org.highmed.numportal.service.notification.dto.NewUserWithoutOrganizationNotification;
Expand All @@ -35,14 +36,14 @@

@Slf4j
@Service
//@org.springframework.transaction.annotation.Transactional(value = "numTransactionManager")
public class UserDetailsService {

private UserDetailsRepository userDetailsRepository;
private OrganizationRepository organizationRepository;
private OrganizationService organizationService;
private NotificationService notificationService;
private UserService userService;
private final UserDetailsRepository userDetailsRepository;
private final OrganizationRepository organizationRepository;
private final OrganizationService organizationService;
private final NotificationService notificationService;
private final UserService userService;
private final UsersMetrics usersMetrics;

private static final String USER_ATTRIBUTE_DEPARTMENT = "department";
private static final String USER_ATTRIBUTE_REQUESTED_ROLE = "requested-role";
Expand All @@ -52,16 +53,18 @@ public class UserDetailsService {

@Autowired
public UserDetailsService(
@Lazy UserService userService,
UserDetailsRepository userDetailsRepository,
OrganizationRepository organizationRepository,
@Lazy OrganizationService organizationService,
NotificationService notificationService) {
@Lazy UserService userService,
UserDetailsRepository userDetailsRepository,
OrganizationRepository organizationRepository,
@Lazy OrganizationService organizationService,
NotificationService notificationService,
UsersMetrics usersMetrics) {
this.userService = userService;
this.notificationService = notificationService;
this.organizationService = organizationService;
this.organizationRepository = organizationRepository;
this.userDetailsRepository = userDetailsRepository;
this.usersMetrics = usersMetrics;
}

protected void deleteUserDetails(String userId) {
Expand Down Expand Up @@ -92,6 +95,7 @@ public UserDetails createUserDetails(String userId, String emailAddress) {
}
// trigger cache update
userService.addUserToCache(userId);
usersMetrics.addNewUserAsUnapproved();
return saved;
}
}
Expand Down Expand Up @@ -146,6 +150,7 @@ public UserDetails approveUser(String loggedInUserId, String userId) {
UserDetails saved = userDetailsRepository.save(userDetails);

notificationService.send(collectAccountApprovalNotification(userId, loggedInUserId));
usersMetrics.approveUser();
log.info("User {} was approved by {}", userId, loggedInUserId);
return saved;
}
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/org/highmed/numportal/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.highmed.numportal.service.exception.ForbiddenException;
import org.highmed.numportal.service.exception.ResourceNotFound;
import org.highmed.numportal.service.exception.SystemException;
import org.highmed.numportal.service.metric.UsersMetrics;
import org.highmed.numportal.service.notification.NotificationService;
import org.highmed.numportal.service.notification.dto.Notification;
import org.highmed.numportal.service.notification.dto.account.RolesUpdateNotification;
Expand Down Expand Up @@ -71,9 +72,11 @@ public class UserService {

private final TranslationRepository translationRepository;

private UsersMetrics usersMetrics;

public static final String TRANSLATION_CACHE = "translation";

private static final String USERS_CACHE = "users";
public static final String USERS_CACHE = "users";

private static final String KEYCLOACK_DEFAULT_ROLES_PREFIX = "default-roles-";

Expand Down Expand Up @@ -642,6 +645,7 @@ public User updateUserActiveField(@NotNull String loggedInUserId, @NotNull Strin
log.debug("Keycloak call to update user's {} 'enabled' field", userId);
keycloakFeign.updateUser(userId, userRaw);
userDetailsService.sendAccountStatusChangedNotification(userId, loggedInUserId, active);
usersMetrics.updateCountStatus(active);
}
return getUserById(userId, true);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.highmed.numportal.service.metric;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import org.highmed.numportal.domain.model.admin.UserDetails;
import org.highmed.numportal.domain.repository.UserDetailsRepository;
import org.highmed.numportal.web.feign.KeycloakFeign;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Optional;

/**
* Custom prometheus metric, to detect number of user in different states, active, inactive or unapproved.
*/
@Getter
@Component
public class UsersMetrics {

private double unapprovedUsers;
private double activeUsers;
private double inactiveUsers;

public UsersMetrics(MeterRegistry registry, UserDetailsRepository userDetailsRepository, KeycloakFeign keycloakFeign) {
Gauge.builder("custom.metric.user.unapproved.counter", this::getUnapprovedUsers)
.description("Unapproved users")
.register(registry);
Gauge.builder("custom.metric.user.active.counter", this::getActiveUsers)
.description("Active users")
.register(registry);
Gauge.builder("custom.metric.user.inactive.counter", this::getInactiveUsers)
.description("Inactive users")
.register(registry);

Optional<List<UserDetails>> unapproved = userDetailsRepository.findAllByApproved(false);
unapproved.ifPresent(userDetails -> this.unapprovedUsers = userDetails.size());
this.inactiveUsers = keycloakFeign.countUsers(false);
// decrease because of service account
this.activeUsers = keycloakFeign.countUsers(true) - 1;

}

public void addNewUserAsUnapproved() {
this.unapprovedUsers++;
}

public void approveUser() {
this.unapprovedUsers--;
}

public void updateCountStatus(@NotNull boolean active) {
if(active) {
this.incrementActiveUsers();
} else {
this.incrementInactiveUsers();
}
}

private void incrementActiveUsers() {
this.activeUsers++;
this.inactiveUsers--;
}

private void incrementInactiveUsers() {
this.inactiveUsers++;
this.activeUsers--;
}
}
11 changes: 5 additions & 6 deletions src/main/java/org/highmed/numportal/web/feign/KeycloakFeign.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

import org.highmed.numportal.domain.model.admin.Role;
import org.highmed.numportal.domain.model.admin.User;

import java.util.Map;
import java.util.Set;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.*;

@FeignClient(name = "keycloak", url = "${userstore.url}")
public interface KeycloakFeign {
Expand All @@ -25,6 +21,9 @@ public interface KeycloakFeign {
@GetMapping("/users/{userId}/role-mappings/realm")
Set<Role> getRolesOfUser(@PathVariable String userId);

@GetMapping("/users/count")
Long countUsers( @RequestParam("enabled") boolean enabled);

@PostMapping("/users/{userId}/role-mappings/realm")
void addRoles(@PathVariable String userId, @RequestBody Role[] role);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.highmed.numportal;

import org.highmed.numportal.listeners.UserCacheInit;
import org.highmed.numportal.service.atna.AtnaProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.highmed.numportal.integrationtesting.config;

import org.mockserver.client.MockServerClient;
import org.mockserver.model.*;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.utility.DockerImageName;

import static org.highmed.numportal.integrationtesting.tests.IntegrationTest.IDENTITY_PROVIDER_TOKEN_ENDPOINT;

public class EhrBaseMockContainer extends MockServerContainer {
private static final String IMAGE_VERSION = "mockserver/mockserver:5.15.0";
private static final String EHR_BASE_URL = "/ehrbase/rest/openehr/v1/definition/template/adl1.4/";
private static EhrBaseMockContainer container;

private EhrBaseMockContainer() {
super(DockerImageName.parse(IMAGE_VERSION));
}

public static EhrBaseMockContainer getInstance() {
if (container == null) {
container = new EhrBaseMockContainer();
}
return container;
}

@Override
public void start() {
super.start();
System.setProperty("EHRBASE_URL", "http://localhost:" + container.getServerPort());

MockServerClient client = new MockServerClient("localhost", container.getServerPort());
client
.when(HttpRequest.request().withMethod("GET").withPath(EHR_BASE_URL))
.respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("[{\"template_id\": \"IDCR - Immunisation summary.v0\",\"concept\": \"IDCR - Immunisation summary.v0\",\"archetype_id\": \"openEHR-EHR-COMPOSITION.health_summary.v1\",\"created_timestamp\": \"2020-11-25T16:19:37.812Z\"}]", MediaType.JSON_UTF_8));
}

@Override
public void stop() {
// do nothing, JVM handles shut down
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.highmed.numportal.integrationtesting.config;

import org.mockserver.client.MockServerClient;
import org.mockserver.model.*;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.utility.DockerImageName;

import static org.highmed.numportal.integrationtesting.tests.IntegrationTest.IDENTITY_PROVIDER_TOKEN_ENDPOINT;

public class KeycloakMockContainer extends MockServerContainer {
private static final String IMAGE_VERSION = "mockserver/mockserver:5.15.0";
public static final String USERS_COUNT = "/admin/realms/Num/users/count";
private static KeycloakMockContainer container;

private KeycloakMockContainer() {
super(DockerImageName.parse(IMAGE_VERSION));
}

public static KeycloakMockContainer getInstance() {
if (container == null) {
container = new KeycloakMockContainer();
}
return container;
}

@Override
public void start() {
super.start();
System.setProperty("KEYCLOAK_URL", "http://localhost:" + container.getServerPort());

MockServerClient client = new MockServerClient("localhost", container.getServerPort());
Header header = new Header("Content-Type", "application/json; charset=utf-8");
Header authHeader = new Header("Authorization", "Bearer {{randomValue length=20 type='ALPHANUMERIC'}}");

client
.when(HttpRequest.request().withMethod("GET").withQueryStringParameter("enabled", "true").withPath(USERS_COUNT))
.respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withHeaders(authHeader).withBody("2", MediaType.JSON_UTF_8));
client
.when(HttpRequest.request().withMethod("GET").withQueryStringParameter("enabled", "false").withPath(USERS_COUNT))
.respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withHeaders(authHeader).withBody("0", MediaType.JSON_UTF_8));
client
.when(HttpRequest.request().withMethod("POST").withPath(IDENTITY_PROVIDER_TOKEN_ENDPOINT))
.respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withHeaders(header).withBody("{\"token_type\": \"Bearer\",\"access_token\":\"{{randomValue length=20 type='ALPHANUMERIC'}}\"}"));
}

@Override
public void stop() {
// do nothing, JVM handles shut down
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import org.testcontainers.containers.PostgreSQLContainer;

public class NumPostgresqlContainer extends PostgreSQLContainer<NumPostgresqlContainer> {
public class PostgresqlContainer extends PostgreSQLContainer<PostgresqlContainer> {
private static final String IMAGE_VERSION = "postgres:12.14";
private static NumPostgresqlContainer container;
private static PostgresqlContainer container;

private NumPostgresqlContainer(String databaseName) {
private PostgresqlContainer(String databaseName) {
super(IMAGE_VERSION);
this.withDatabaseName(databaseName);
}

public static NumPostgresqlContainer getInstance(String databaseName) {
public static PostgresqlContainer getInstance(String databaseName) {
if (container == null) {
container = new NumPostgresqlContainer(databaseName);
container = new PostgresqlContainer(databaseName);
}
return container;
}
Expand Down
Loading

0 comments on commit 40bc1c1

Please sign in to comment.