Skip to content

Commit

Permalink
Add JPA statistics support for eclipselink and hibernate
Browse files Browse the repository at this point in the history
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
  • Loading branch information
avgustinmm committed Jan 20, 2025
1 parent 916b0ce commit c37de14
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 12 deletions.
6 changes: 6 additions & 0 deletions hawkbit-repository/hawkbit-repository-jpa-eclipselink/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<optional>true</optional>
</dependency>

<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;

import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jakarta.persistence.EntityManagerFactory;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.Getter;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.tools.profiler.PerformanceMonitor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;

/**
* (Experimental) Report EclipseLink statistics to Micrometer.
* <p/>
* To be enabled:
* <ol>
* <li>The Spring property spring.jpa.properties.eclipselink.profiler=PerformanceMonitor shall be set - enables Eclipselink statistics
* collecting</li>
* <li>By default the stdout log is disabled by setting hawkbit.jpa.statistics.dumpPeriodMS=9223372036854775807 (Long.MAX_VALUE) -
* i.e. effectively <b>never</b>. If log is required it should be set to the required period</li>
* <li>The MeterRegistry shall be registered available - e.g. include org.springframework.boot:spring-boot-actuator-autoconfigure</li>
* <li>(?) When using in test the metrics MAYBE shall be enabled with @AutoConfigureObservability(tracing = false)</li>
* </ol>
*
* It encapsulates reporting the Eclipselink {@link PerformanceMonitor} statistics to the {@link MeterRegistry} and the Spring autoconfiguration.
*/
public class Statistics {

public static final String METER_PREFIX = "eclipselink.";

private static final Statistics INSTANCE = new Statistics();

private static final Pattern PATTERN = Pattern.compile("(?<type>(Counter|Timer)+):(?<key>[^ -]+)");
private static final Map<String, Long> REPORTED_TIMER_VALUES = new HashMap<>();

private EntityManagerFactory entityManagerFactory;
// if meter registry is unavailable, the statistics will not send to metrics
@Getter
private MeterRegistry meterRegistry;

private boolean flushing;

/**
* @return the singleton {@link Statistics} instance
*/
public static Statistics getInstance() {
return INSTANCE;
}

@Autowired
public void setEntityManagerFactory(
final EntityManagerFactory entityManagerFactory,
@Value("${hawkbit.jpa.statistics.dumpPeriodMS:9223372036854775807}") final long dumpPeriod) {
this.entityManagerFactory = entityManagerFactory;
// set stdout log PerformanceMonitor. By default, it is Long.MAX_VALUE (9223372036854775807) which effectively disable logging
getPerformanceMonitor(entityManagerFactory).setDumpTime(dumpPeriod);
}

@Autowired
public void setMeterRegistry(final MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

// flushes the statistics to the meter registry (if needed)
public static void flush() {
final MeterRegistry meterRegistry = INSTANCE.meterRegistry;
if (meterRegistry == null) {
// not a bean (i.e. no performance monitoring) is enabled or no meter registry available
return;
}

synchronized (INSTANCE) {
if (INSTANCE.flushing) {
// wait for flushing
do {
try {
INSTANCE.wait(1000);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (INSTANCE.flushing);
// flushed
return;
}
// flush
INSTANCE.flushing = true;
}
INSTANCE.flush0();
}

@Scheduled(initialDelayString = "${hawkbit.jpa.statistics.flush.fixedDelay:60000}", fixedDelayString = "${hawkbit.jpa.statistics.flush.fixedDelay:60000}")
void periodicFlush() {
if (meterRegistry == null) {
// meter registry available
return;
}

synchronized (this) {
if (flushing) {
// no need to wait for flushing
return;
}
// flush
flushing = true;
}
flush0();
}

private void flush0() {
final PerformanceMonitor performanceMonitor = getPerformanceMonitor(entityManagerFactory);
final Map<String, Object> opTimings = performanceMonitor.getOperationTimings();
opTimings.forEach((k, v) -> {
if (opTimings.keySet().stream().anyMatch(key -> !key.equals(k) && key.startsWith(k))) {
// it is a group, e.g.:
// Timer:ReportQuery 65,402,376
// Timer:ReportQuery:org.eclipse.hawkbit.repository.jpa.model.DistributionSetTypeElement:null:QueryPreparation 177,375
// Timer:ReportQuery:org.eclipse.hawkbit.repository.jpa.model.DistributionSetTypeElement:null:SqlGeneration 36,083
// Counter:ReportQuery 56
// Counter:ReportQuery:org.eclipse.hawkbit.repository.jpa.model.JpaTenantMetaData:null 56
// we want to report per tag/operation, not the group - the sum could be made on the metric collector side (e.g. prometheus)
return;
}

final Matcher matcher = PATTERN.matcher(k);
if (matcher.matches()) {
final String type = matcher.group("type");
final StringTokenizer stringTokenizer = new StringTokenizer(matcher.group("key"), ":");
final String name = METER_PREFIX + stringTokenizer.nextToken();
if (type.equals("Counter")) {
final double quantity = v instanceof Double d ? d : Double.parseDouble(v.toString());
final Counter counter;
if (stringTokenizer.hasMoreTokens()) {
counter = meterRegistry.counter(name, "entity", stringTokenizer.nextToken());
} else {
counter = meterRegistry.counter(name);
}
counter.increment(quantity - counter.count());
} else { // Timer
final long quantity = v instanceof Long l ? l : (long) Double.parseDouble(v.toString());
final Timer timer;
if (stringTokenizer.hasMoreTokens()) {
final String entity = stringTokenizer.nextToken();
stringTokenizer.nextToken(); // skip, what is this?
final String subOp = stringTokenizer.hasMoreTokens() ? stringTokenizer.nextToken() : "n/a";
timer = meterRegistry.timer(name, "entity", entity, "subOp", subOp);
} else {
timer = meterRegistry.timer(name);
}
timer.record(quantity - REPORTED_TIMER_VALUES.getOrDefault(name, 0L), TimeUnit.NANOSECONDS);
REPORTED_TIMER_VALUES.put(name, quantity);
}
}
});

synchronized (this) {
if (flushing) {
flushing = false;
}
this.notifyAll();
}
}

private static PerformanceMonitor getPerformanceMonitor(final EntityManagerFactory entityManagerFactory) {
return (PerformanceMonitor) entityManagerFactory.unwrap(Session.class).getProfiler();
}

// autoconfigure after CompositeMeterRegistryAutoConfiguration, so when the autoconfiguration is being processed the MeterRegistry
// has already been registered / resolved (if it is to be registered at all) - otherwise @ConditionalOnBean(MeterRegistry.class) may not be
// met event if the MeterRegistry is registered (if resolved later).
// 'autoconfigure after' relies on this is being an AutoConfiguration
@AutoConfiguration(afterName = "org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration")
@Configuration
public static class StatisticsAutoConfiguration {

@ConditionalOnProperty(prefix = "spring.jpa.properties.eclipselink", name = "profiler", havingValue = "PerformanceMonitor")
@ConditionalOnBean(MeterRegistry.class)
@Bean
public Statistics statistics() {
// injects the singleton Statistics, and start scheduler
return Statistics.getInstance();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.eclipse.hawkbit.repository.jpa.Statistics.StatisticsAutoConfiguration
6 changes: 6 additions & 0 deletions hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
<artifactId>hibernate-core</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<optional>true</optional>
</dependency>

<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;

import io.micrometer.core.instrument.MeterRegistry;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* (Experimental) Report Hibernate statistics to Micrometer.
* <p/>
* To be enabled:
* <ol>
* <li>The Spring property spring.jpa.properties.hibernate.generate_statistics=true shall be set - enables Hibernate statistics
* collecting</li>
* <li>If don't need log in the stdout set logging.level.org.hibernate.engine.internal.StatisticalLoggingSessionEventListener=WARN</li>
* <li>The MeterRegistry shall be registered available - e.g. include org.springframework.boot:spring-boot-actuator-autoconfigure</li>
* <li>Hibernate reporting to micrometer shall be enabled - include org.hibernate.orm:hibernate-micrometer</li>
* <li>(?) When using in test the metrics MAYBE shall be enabled with @AutoConfigureObservability(tracing = false)</li>
* </ol>
*/
public class Statistics {

public static final String METER_PREFIX = "hibernate.";

private static final Statistics INSTANCE = new Statistics();

// if meter registry is unavailable, the statistics will not send to metrics
@Getter
public MeterRegistry meterRegistry;

/**
* @return the singleton {@link Statistics} instance
*/
public static Statistics getInstance() {
return INSTANCE;
}

@Autowired
public void setMeterRegistry(final MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

// flushes the statistics to the meter registry (if needed)
public static void flush() {
// nothing to do for Hibernate
}

// autoconfigure after CompositeMeterRegistryAutoConfiguration, so when the autoconfiguration is being processed the MeterRegistry
// has already been registered / resolved (if it is to be registered at all) - otherwise @ConditionalOnBean(MeterRegistry.class) may not be
// met event if the MeterRegistry is registered (if resolved later).
// 'autoconfigure after' relies on this is being an AutoConfiguration
@AutoConfiguration(afterName = "org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration")
@Configuration
public static class StatisticsAutoConfiguration {

@ConditionalOnProperty(prefix = "spring.jpa.properties.hibernate", name = "generate_statistics", havingValue = "true")
@ConditionalOnBean(MeterRegistry.class)
@Bean
public Statistics statistics() {
// injects the singleton Statistics, and start scheduler
return Statistics.getInstance();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.eclipse.hawkbit.repository.jpa.Statistics.StatisticsAutoConfiguration
19 changes: 19 additions & 0 deletions hawkbit-repository/hawkbit-repository-jpa/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<optional>true</optional>
</dependency>

<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
Expand Down Expand Up @@ -116,5 +122,18 @@
<artifactId>javax.el-api</artifactId>
<scope>test</scope>
</dependency>

<!-- Enable metrics -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<!-- Enable metrics for hibernates -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-micrometer</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -869,8 +869,7 @@ AutoAssignExecutor autoAssignExecutor(final TargetFilterQueryManagement targetFi
*/
@Bean
@ConditionalOnMissingBean
// don't active the auto assign scheduler in test, otherwise it is hard to
// test
// don't active the auto assign scheduler in test, otherwise it is hard to test
@Profile("!test")
@ConditionalOnProperty(prefix = "hawkbit.autoassign.scheduler", name = "enabled", matchIfMissing = true)
AutoAssignScheduler autoAssignScheduler(final SystemManagement systemManagement,
Expand Down
Loading

0 comments on commit c37de14

Please sign in to comment.