Skip to content

Commit aa79f6c

Browse files
Merge PR #443: Replace Micrometer with OpenTelemetry
2 parents 914d389 + 5590460 commit aa79f6c

26 files changed

Lines changed: 177 additions & 259 deletions

backend/pom.xml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,7 @@
128128
</dependency>
129129
<dependency>
130130
<groupId>io.quarkus</groupId>
131-
<artifactId>quarkus-micrometer</artifactId>
132-
</dependency>
133-
<dependency>
134-
<groupId>io.quarkus</groupId>
135-
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
131+
<artifactId>quarkus-opentelemetry</artifactId>
136132
</dependency>
137133
</dependencies>
138134

backend/src/main/java/org/cryptomator/hub/api/AuditLogResource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.fasterxml.jackson.annotation.JsonProperty;
44
import com.fasterxml.jackson.annotation.JsonSubTypes;
55
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6+
import io.opentelemetry.instrumentation.annotations.WithSpan;
67
import jakarta.annotation.security.RolesAllowed;
78
import jakarta.inject.Inject;
89
import jakarta.ws.rs.BadRequestException;
@@ -74,6 +75,7 @@ public class AuditLogResource {
7475
@APIResponse(responseCode = "400", description = "startDate or endDate not specified, startDate > endDate, order specified and not in ['asc','desc'], pageSize not in [1 .. 100] or type is not valid")
7576
@APIResponse(responseCode = "402", description = "Community license used or license expired")
7677
@APIResponse(responseCode = "403", description = "requesting user does not have admin role")
78+
@WithSpan("AuditLogResource.getAllEvents")
7779
public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDate, @QueryParam("endDate") Instant endDate, @QueryParam("type") List<String> type, @QueryParam("paginationId") Long paginationId, @QueryParam("order") @DefaultValue("desc") String order, @QueryParam("pageSize") @DefaultValue("20") int pageSize) {
7880
if (license.getEntitlements().auditLogRetentionDays() == 0 || license.isExpired()) {
7981
throw new PaymentRequiredException("Community license used or license expired");

backend/src/main/java/org/cryptomator/hub/entities/EffectiveGroupMembership.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cryptomator.hub.entities;
22

3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
34
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
45
import io.quarkus.panache.common.Parameters;
56
import jakarta.enterprise.context.ApplicationScoped;
@@ -87,6 +88,7 @@ public void fullUpdate() {
8788
.executeUpdate();
8889
}
8990

91+
@WithSpan("EffectiveGroupMembership.Repository.updateGroups")
9092
public void updateGroups(Collection<String> groupIds) {
9193
Batch.of(200).run(groupIds, batch -> {
9294
getEntityManager()
@@ -100,6 +102,7 @@ public void updateGroups(Collection<String> groupIds) {
100102
});
101103
}
102104

105+
@WithSpan("EffectiveGroupMembership.Repository.updateUsers")
103106
public void updateUsers(Collection<String> userIds) {
104107
Batch.of(200).run(userIds, batch -> {
105108
delete("#EffectiveGroupMembership.deleteUsers", Parameters.with("userIds", batch));

backend/src/main/java/org/cryptomator/hub/entities/EffectiveVaultAccess.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cryptomator.hub.entities;
22

3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
34
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
45
import io.quarkus.panache.common.Parameters;
56
import jakarta.enterprise.context.ApplicationScoped;
@@ -131,6 +132,7 @@ public long countSeatOccupyingUsers() {
131132
return count("#EffectiveVaultAccess.countSeatOccupyingUsers");
132133
}
133134

135+
@WithSpan("EffectiveVaultAccess.Repository.countSeatOccupyingUsersWithAccessToken")
134136
public long countSeatOccupyingUsersWithAccessToken() {
135137
return count("#EffectiveVaultAccess.countSeatOccupyingUsersWithAccessToken");
136138
}

backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAdminService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cryptomator.hub.keycloak;
22

3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
34
import jakarta.annotation.PostConstruct;
45
import jakarta.enterprise.context.ApplicationScoped;
56
import jakarta.inject.Inject;
@@ -61,6 +62,7 @@ public void setup() {
6162
this.realm = keycloak.realm(keycloakRealm);
6263
}
6364

65+
@WithSpan("KeycloakAdminService.createUser")
6466
public UserRepresentation createUser(String username, String email, String firstName, String lastName, String password, String pictureUrl, Set<String> groupIds) {
6567
UserRepresentation user = new UserRepresentation();
6668
user.setUsername(username);
@@ -199,6 +201,7 @@ public boolean isUserReadOnly(String userId) {
199201

200202
// TODO deduplicate with KeycloakAuthorityPuller
201203
@Transactional
204+
@WithSpan("KeycloakAdminService.syncUser")
202205
public User syncUser(String userId) {
203206
UserResource userResource = realm.users().get(userId);
204207
UserRepresentation keycloakUser = userResource.toRepresentation();
@@ -230,6 +233,7 @@ public User syncUser(String userId) {
230233

231234
// TODO deduplicate with KeycloakAuthorityPuller
232235
@Transactional
236+
@WithSpan("KeycloakAdminService.syncGroup")
233237
public Group syncGroup(String groupId) {
234238
GroupResource groupResource = realm.groups().group(groupId);
235239
GroupRepresentation keycloakGroup = groupResource.toRepresentation();
@@ -285,6 +289,7 @@ public void removeUserFromGroup(String groupId, String userId) {
285289
}
286290

287291
@Transactional
292+
@WithSpan("KeycloakAdminService.updateUserRoles")
288293
public void updateUserRoles(String userId, Set<RealmRole> roles) {
289294
// remove roles that are not in the provided set:
290295
var rolesToRemove = EnumSet.allOf(RealmRole.class);

backend/src/main/java/org/cryptomator/hub/keycloak/KeycloakAuthorityPuller.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cryptomator.hub.keycloak;
22

3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
34
import io.quarkus.scheduler.Scheduled;
45
import jakarta.enterprise.context.ApplicationScoped;
56
import jakarta.inject.Inject;
@@ -30,6 +31,7 @@ public class KeycloakAuthorityPuller {
3031
EffectiveGroupMembership.Repository effectiveGroupMembershipRepo;
3132

3233
@Scheduled(every = "{hub.keycloak.syncer-period}")
34+
@WithSpan("KeycloakAuthorityPuller.sync")
3335
void sync() {
3436
var keycloakGroups = remoteUserProvider.groups().stream().collect(Collectors.toMap(KeycloakGroupDto::id, Function.identity()));
3537
var keycloakUsers = remoteUserProvider.users().stream().collect(Collectors.toMap(KeycloakUserDto::id, Function.identity()));

backend/src/main/java/org/cryptomator/hub/license/LicenseHolder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.auth0.jwt.exceptions.JWTVerificationException;
44
import com.auth0.jwt.interfaces.DecodedJWT;
5+
import io.opentelemetry.instrumentation.annotations.WithSpan;
56
import io.quarkus.scheduler.Scheduled;
67
import io.smallrye.common.annotation.RunOnVirtualThread;
78
import jakarta.annotation.PostConstruct;
@@ -97,6 +98,7 @@ void init() {
9798
* @throws JWTVerificationException if the license is invalid
9899
*/
99100
@Transactional
101+
@WithSpan("LicenseHolder.ensureLicenseExists")
100102
DecodedJWT ensureLicenseExists() throws JWTVerificationException, WebApplicationException {
101103
var settings = settingsRepo.get();
102104
if (settings.getLicenseKey() != null && settings.getHubId() != null) {
@@ -136,6 +138,7 @@ DecodedJWT validateAndApplyInitLicense(Settings settings, String initialLicenseT
136138
}
137139

138140
@Transactional(Transactional.TxType.MANDATORY)
141+
@WithSpan("LicenseHolder.requestAnonTrialLicense")
139142
DecodedJWT requestAnonTrialLicense(Settings settings) throws WebApplicationException {
140143
LOG.info("No license found. Requesting trial license...");
141144
var solution = solveChallenge();
Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,73 @@
11
package org.cryptomator.hub.metrics;
22

3-
import io.micrometer.core.instrument.Gauge;
4-
import io.micrometer.core.instrument.MeterRegistry;
5-
import io.quarkus.scheduler.Scheduled;
6-
import jakarta.annotation.PostConstruct;
3+
import io.opentelemetry.api.common.AttributeKey;
4+
import io.opentelemetry.api.common.Attributes;
5+
import io.opentelemetry.api.metrics.Meter;
6+
import io.opentelemetry.api.metrics.ObservableLongGauge;
7+
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
8+
import io.quarkus.narayana.jta.QuarkusTransaction;
9+
import io.quarkus.runtime.ShutdownEvent;
710
import jakarta.enterprise.context.ApplicationScoped;
11+
import jakarta.enterprise.event.Observes;
812
import jakarta.inject.Inject;
9-
import jakarta.transaction.Transactional;
1013
import org.cryptomator.hub.entities.Device;
1114
import org.cryptomator.hub.entities.EffectiveVaultAccess;
1215
import org.cryptomator.hub.entities.Vault;
1316

14-
import java.util.EnumMap;
15-
import java.util.Map;
16-
import java.util.concurrent.atomic.AtomicLong;
17-
1817
@ApplicationScoped
1918
public class SystemUsageMetrics {
2019

21-
private static final String VAULTS_TOTAL_METRIC = "hub_vaults_total";
22-
private static final String ACTIVE_USERS_TOTAL_METRIC = "hub_active_users_total";
23-
private static final String DEVICES_TOTAL_METRIC = "hub_devices_total";
24-
25-
@Inject
26-
MeterRegistry meterRegistry;
27-
28-
@Inject
29-
Vault.Repository vaultRepo;
20+
private static final String VAULTS_METRIC = "hub_vaults";
21+
private static final String ACTIVE_USERS_METRIC = "hub_active_users";
22+
private static final String DEVICES_METRIC = "hub_devices";
23+
private static final AttributeKey<String> DEVICE_TYPE_KEY = AttributeKey.stringKey("type");
3024

31-
@Inject
32-
EffectiveVaultAccess.Repository effectiveVaultAccessRepo;
25+
private final Vault.Repository vaultRepo;
26+
private final EffectiveVaultAccess.Repository effectiveVaultAccessRepo;
27+
private final Device.Repository deviceRepo;
28+
private final ObservableLongGauge vaultsGauge;
29+
private final ObservableLongGauge activeUsersGauge;
30+
private final ObservableLongGauge devicesGauge;
3331

3432
@Inject
35-
Device.Repository deviceRepo;
36-
37-
private final AtomicLong vaultsTotal = new AtomicLong(0);
38-
private final AtomicLong activeUsersTotal = new AtomicLong(0);
39-
private final Map<Device.Type, AtomicLong> devicesPerType = new EnumMap<>(Device.Type.class);
33+
SystemUsageMetrics(Meter meter, Vault.Repository vaultRepo, EffectiveVaultAccess.Repository effectiveVaultAccessRepo, Device.Repository deviceRepo) {
34+
this.vaultRepo = vaultRepo;
35+
this.effectiveVaultAccessRepo = effectiveVaultAccessRepo;
36+
this.deviceRepo = deviceRepo;
37+
this.vaultsGauge = meter.gaugeBuilder(VAULTS_METRIC)
38+
.ofLongs()
39+
.setDescription("Number of vaults")
40+
.buildWithCallback(this::recordVaultCount);
41+
this.activeUsersGauge = meter.gaugeBuilder(ACTIVE_USERS_METRIC)
42+
.ofLongs()
43+
.setDescription("Number of unique users with access to any non-archived vault")
44+
.buildWithCallback(this::recordSeatCount);
45+
this.devicesGauge = meter.gaugeBuilder(DEVICES_METRIC)
46+
.ofLongs()
47+
.setDescription("Number of devices grouped by type")
48+
.buildWithCallback(this::recordDeviceCount);
49+
}
4050

41-
@PostConstruct
42-
void registerMetrics() {
43-
Gauge.builder(VAULTS_TOTAL_METRIC, vaultsTotal, AtomicLong::get)
44-
.description("Number of vaults")
45-
.register(meterRegistry);
51+
private void recordVaultCount(ObservableLongMeasurement measurement) {
52+
measurement.record(QuarkusTransaction.requiringNew().call(vaultRepo::count));
53+
}
4654

47-
Gauge.builder(ACTIVE_USERS_TOTAL_METRIC, activeUsersTotal, AtomicLong::get)
48-
.description("Number of unique users with access to any non-archived vault")
49-
.register(meterRegistry);
55+
private void recordSeatCount(ObservableLongMeasurement measurement) {
56+
measurement.record(QuarkusTransaction.requiringNew().call(effectiveVaultAccessRepo::countSeatOccupyingUsers));
57+
}
5058

51-
for (var deviceType : Device.Type.values()) {
52-
var value = new AtomicLong(0);
53-
devicesPerType.put(deviceType, value);
54-
Gauge.builder(DEVICES_TOTAL_METRIC, value, AtomicLong::get)
55-
.description("Number of devices grouped by type")
56-
.tag("type", deviceType.name())
57-
.register(meterRegistry);
58-
}
59+
private void recordDeviceCount(ObservableLongMeasurement measurement) {
60+
QuarkusTransaction.requiringNew().run(() -> {
61+
for (var type : Device.Type.values()) {
62+
measurement.record(deviceRepo.count("type", type), Attributes.of(DEVICE_TYPE_KEY, type.name()));
63+
}
64+
});
5965
}
6066

61-
@Scheduled(every = "24h", delayed = "10s")
62-
@Transactional
63-
void collect() {
64-
vaultsTotal.set(vaultRepo.count());
65-
activeUsersTotal.set(effectiveVaultAccessRepo.countSeatOccupyingUsers());
66-
for (var deviceType : Device.Type.values()) {
67-
var count = deviceRepo.count("type", deviceType);
68-
devicesPerType.get(deviceType).set(count);
69-
}
67+
void onStop(@Observes ShutdownEvent event) {
68+
vaultsGauge.close();
69+
activeUsersGauge.close();
70+
devicesGauge.close();
7071
}
72+
7173
}

backend/src/main/java/org/cryptomator/hub/metrics/VaultUnlockMetrics.java

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,47 @@
11
package org.cryptomator.hub.metrics;
22

3-
import io.micrometer.core.instrument.Counter;
4-
import io.micrometer.core.instrument.Gauge;
5-
import io.micrometer.core.instrument.MeterRegistry;
6-
import jakarta.annotation.PostConstruct;
3+
import io.opentelemetry.api.metrics.LongCounter;
4+
import io.opentelemetry.api.metrics.LongGauge;
5+
import io.opentelemetry.api.metrics.Meter;
76
import jakarta.enterprise.context.ApplicationScoped;
87
import jakarta.inject.Inject;
98

10-
import java.util.concurrent.atomic.AtomicLong;
11-
129
@ApplicationScoped
1310
public class VaultUnlockMetrics {
1411

1512
private static final String UNLOCKS_TOTAL_METRIC = "hub_vault_unlocks_total";
1613
private static final String LAST_SUCCESS_METRIC = "hub_vault_unlock_last_success_epoch_seconds";
1714
private static final String LAST_FAILURE_METRIC = "hub_vault_unlock_last_failure_epoch_seconds";
1815

16+
private final LongCounter unlockCounter;
17+
private final LongGauge lastSuccessGauge;
18+
private final LongGauge lastFailureGauge;
19+
1920
@Inject
20-
MeterRegistry meterRegistry;
21-
22-
private final AtomicLong lastSuccessEpochSeconds = new AtomicLong(0);
23-
private final AtomicLong lastFailureEpochSeconds = new AtomicLong(0);
24-
private Counter unlockCounter;
25-
26-
@PostConstruct
27-
void registerGauges() {
28-
unlockCounter = Counter.builder(UNLOCKS_TOTAL_METRIC)
29-
.description("Total number of vault unlock attempts")
30-
.register(meterRegistry);
31-
Gauge.builder(LAST_SUCCESS_METRIC, lastSuccessEpochSeconds, AtomicLong::get)
32-
.description("Epoch timestamp in seconds of the last successful vault unlock")
33-
.register(meterRegistry);
34-
Gauge.builder(LAST_FAILURE_METRIC, lastFailureEpochSeconds, AtomicLong::get)
35-
.description("Epoch timestamp in seconds of the last failed vault unlock")
36-
.register(meterRegistry);
21+
VaultUnlockMetrics(Meter meter) {
22+
this.unlockCounter = meter.counterBuilder(UNLOCKS_TOTAL_METRIC)
23+
.setDescription("Total number of vault unlock attempts")
24+
.build();
25+
this.lastSuccessGauge = meter.gaugeBuilder(LAST_SUCCESS_METRIC)
26+
.ofLongs()
27+
.setDescription("Epoch timestamp in seconds of the last successful vault unlock")
28+
.build();
29+
this.lastFailureGauge = meter.gaugeBuilder(LAST_FAILURE_METRIC)
30+
.ofLongs()
31+
.setDescription("Epoch timestamp in seconds of the last failed vault unlock")
32+
.build();
3733
}
3834

3935
public void recordUnlock() {
40-
unlockCounter.increment();
36+
unlockCounter.add(1);
4137
}
4238

4339
public void recordSuccess() {
44-
lastSuccessEpochSeconds.set(currentEpochSeconds());
40+
lastSuccessGauge.set(currentEpochSeconds());
4541
}
4642

4743
public void recordFailure() {
48-
lastFailureEpochSeconds.set(currentEpochSeconds());
44+
lastFailureGauge.set(currentEpochSeconds());
4945
}
5046

5147
private long currentEpochSeconds() {

backend/src/main/resources/application.properties

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ quarkus.swagger-ui.enabled=false
5959
# Management Interface
6060
quarkus.management.enabled=true
6161
quarkus.management.port=9000
62-
## Metrics
63-
quarkus.micrometer.enabled=true
64-
quarkus.micrometer.export.prometheus.enabled=true
65-
quarkus.datasource.metrics.enabled=true
62+
# OpenTelemetry. Tracing is on by default; metrics and logs are opt-in.
63+
quarkus.otel.metrics.enabled=true
64+
quarkus.otel.logs.enabled=true
65+
%test.quarkus.otel.sdk.disabled=true
6666
## Health
6767
quarkus.smallrye-health.enabled=true
6868
quarkus.datasource.health.enabled=true

0 commit comments

Comments
 (0)