SystemInformationService.java
package com.taxonomy.shared.service;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.tool.schema.Action;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** On-demand, allowlisted diagnostics. No paths, URLs, credentials or exception text leave this service. */
@Service
public class SystemInformationService {
private static final String INSTANCE_ID = java.util.UUID.randomUUID().toString();
private final EntityManagerFactory entityManagerFactory;
private final Environment environment;
public SystemInformationService(EntityManagerFactory entityManagerFactory, Environment environment) {
this.entityManagerFactory = entityManagerFactory;
this.environment = environment;
}
public Snapshot snapshot() {
Runtime runtime = Runtime.getRuntime();
var jvm = ManagementFactory.getRuntimeMXBean();
String indexStorage = normalizeIndexStorage(environment.getProperty(
"spring.jpa.properties.hibernate.search.backend.directory.type", "unknown"));
return new Snapshot(Instant.now(), INSTANCE_ID, environment.getProperty("app.display-version", "unknown"),
new RuntimeInfo(runtime.availableProcessors(), runtime.totalMemory() - runtime.freeMemory(),
runtime.maxMemory(), jvm.getUptime(), Instant.ofEpochMilli(jvm.getStartTime()),
System.getProperty("java.version"), System.getProperty("java.vendor"),
System.getProperty("os.name"), System.getProperty("os.arch")),
database(), indexStorage, disks(indexStorage));
}
DatabaseInfo database() {
List<String> warnings = new ArrayList<>();
String schemaAction = "UNKNOWN";
String product = null;
String version = null;
String driver = null;
String driverVersion = null;
String versionSource = "UNAVAILABLE";
String storageSource = "UNAVAILABLE";
String storage = "UNKNOWN";
String lifetime = "UNKNOWN";
String status = "UNAVAILABLE";
try {
SessionFactory factory = entityManagerFactory.unwrap(SessionFactory.class);
schemaAction = schemaAction(factory.getProperties());
if (destructive(schemaAction)) {
warnings.add("DESTRUCTIVE_SCHEMA_ACTION");
} else if ("UNKNOWN".equals(schemaAction)) {
warnings.add("SCHEMA_ACTION_UNKNOWN");
}
try (Session session = factory.openSession()) {
session.setDefaultReadOnly(true);
session.setHibernateFlushMode(FlushMode.MANUAL);
Metadata metadata = session.doReturningWork(connection -> {
var md = connection.getMetaData();
return new Metadata(md.getDatabaseProductName(), md.getDatabaseProductVersion(),
md.getDriverName(), md.getDriverVersion(), md.getURL());
});
product = metadata.product();
version = metadata.version();
driver = metadata.driver();
driverVersion = metadata.driverVersion();
versionSource = version == null ? "UNAVAILABLE" : "JDBC_METADATA_FALLBACK";
var registry = factory.unwrap(SessionFactoryImplementor.class)
.getQueryEngine().getSqmFunctionRegistry();
if (registry.findFunctionDescriptor(DatabaseDiagnosticFunctions.VERSION) != null) {
String queriedVersion = query(session, DatabaseDiagnosticFunctions.VERSION);
if (queriedVersion != null && !queriedVersion.isBlank()) {
version = queriedVersion;
versionSource = "DATABASE_QUERY";
}
String queriedStorage = registry.findFunctionDescriptor(DatabaseDiagnosticFunctions.STORAGE) == null
? null : query(session, DatabaseDiagnosticFunctions.STORAGE);
if (List.of("IN_MEMORY", "FILE_BACKED", "READ_ONLY_RESOURCE", "SERVER_MANAGED")
.contains(queriedStorage == null ? "" : queriedStorage)) {
storage = queriedStorage;
storageSource = "DATABASE_QUERY";
}
}
if ("UNKNOWN".equals(storage)) {
storage = connectionStorage(metadata.url());
if (!"UNKNOWN".equals(storage)) {
storageSource = "JDBC_CONNECTION";
}
}
if ("IN_MEMORY".equals(storage)) {
// A remote HSQL server may itself run an in-memory database.
// Restarting Taxonomy is not the same as restarting that server.
lifetime = "IN_MEMORY".equals(connectionStorage(metadata.url()))
? "APPLICATION_PROCESS" : "DATABASE_PROCESS";
warnings.add("IN_MEMORY_" + lifetime);
} else if ("FILE_BACKED".equals(storage) || "SERVER_MANAGED".equals(storage)) {
lifetime = "STORAGE_DEPENDENT";
warnings.add("STORAGE_DURABILITY_UNVERIFIED");
}
status = "DATABASE_QUERY".equals(versionSource) && !"UNKNOWN".equals(storage)
? "AVAILABLE" : "PARTIAL";
}
} catch (RuntimeException exception) {
// A failed diagnostic must not hide the other system information.
// Exception text may include a connection URL or secret: never return it.
warnings.add("DATABASE_DIAGNOSTICS_UNAVAILABLE");
}
if ("UNKNOWN".equals(storage)) {
warnings.add("PERSISTENCE_UNKNOWN");
}
if (environment.getProperty("taxonomy.init.reload-existing", Boolean.class, false)) {
warnings.add("CATALOGUE_RELOAD_ENABLED");
}
return new DatabaseInfo(status, product, version, versionSource, driver, driverVersion,
storage, storageSource, lifetime, schemaAction, List.copyOf(warnings));
}
private static String query(Session session, String function) {
try {
String value = session.createSelectionQuery("select " + function + "()", String.class)
.setTimeout(2).setCacheable(false).getSingleResult();
return value == null ? null : value.strip();
} catch (RuntimeException exception) {
return null; // Visible source flags identify the metadata/unknown fallback.
}
}
static String normalizeIndexStorage(String value) {
String normalized = value == null ? "" : value.strip().toLowerCase(Locale.ROOT);
return switch (normalized) {
case "local-heap", "local-filesystem" -> normalized;
default -> "unknown";
};
}
static String connectionStorage(String url) {
String value = url == null ? "" : url.toLowerCase(Locale.ROOT);
if (value.startsWith("jdbc:hsqldb:mem:") || value.startsWith("jdbc:h2:mem:")) {
return "IN_MEMORY";
}
if (value.startsWith("jdbc:hsqldb:file:") || value.startsWith("jdbc:h2:file:")) {
return "FILE_BACKED";
}
if (value.startsWith("jdbc:hsqldb:res:")) {
return "READ_ONLY_RESOURCE";
}
if (value.startsWith("jdbc:postgresql:") || value.startsWith("jdbc:sqlserver:")
|| value.startsWith("jdbc:oracle:")) {
return "SERVER_MANAGED";
}
return "UNKNOWN";
}
static String schemaAction(Map<String, Object> settings) {
try {
Object jpaAction = settings.get("jakarta.persistence.schema-generation.database.action");
Action action = jpaAction != null
? Action.interpretJpaSetting(jpaAction)
: Action.interpretHbm2ddlSetting(settings.get("hibernate.hbm2ddl.auto"));
return action.name();
} catch (IllegalArgumentException exception) {
return "UNKNOWN";
}
}
static boolean destructive(String action) {
return List.of("CREATE", "CREATE_DROP", "DROP", "TRUNCATE").contains(action);
}
List<DiskInfo> disks(String indexStorage) {
Map<Path, DiskInfo> locations = new LinkedHashMap<>();
List<DiskInfo> unavailable = new ArrayList<>();
addDisk("TEMPORARY_FILES", System.getProperty("java.io.tmpdir"), locations, unavailable);
if ("local-filesystem".equals(indexStorage)) {
addDisk("SEARCH_INDEX", environment.getProperty(
"spring.jpa.properties.hibernate.search.backend.directory.root"), locations, unavailable);
}
List<DiskInfo> result = new ArrayList<>(locations.values());
result.addAll(unavailable);
return List.copyOf(result);
}
private static void addDisk(String purpose, String path, Map<Path, DiskInfo> locations,
List<DiskInfo> unavailable) {
try {
if (path == null || path.isBlank()) {
unavailable.add(new DiskInfo(List.of(purpose), null, null, "UNAVAILABLE"));
return;
}
// FileStore equality/name/type do not portably identify a volume.
// Merge only the same resolved directory. Other rows may share space:
// the UI states this explicitly and never computes an aggregate total.
Path location = Path.of(path).toRealPath();
DiskInfo previous = locations.get(location);
List<String> purposes = new ArrayList<>();
if (previous != null) {
purposes.addAll(previous.purposes());
purposes.add(purpose);
locations.put(location, new DiskInfo(List.copyOf(purposes),
previous.totalBytes(), previous.usableBytes(), previous.status()));
return;
}
FileStore store = Files.getFileStore(location);
purposes.add(purpose);
locations.put(location, new DiskInfo(List.copyOf(purposes), store.getTotalSpace(),
store.getUsableSpace(), "AVAILABLE"));
} catch (java.io.IOException | RuntimeException exception) {
unavailable.add(new DiskInfo(List.of(purpose), null, null, "UNAVAILABLE"));
}
}
private record Metadata(String product, String version, String driver, String driverVersion, String url) { }
public record Snapshot(Instant timestamp, String instanceId, String applicationVersion, RuntimeInfo runtime,
DatabaseInfo database, String indexStorage, List<DiskInfo> disks) { }
public record RuntimeInfo(int availableProcessors, long heapUsedBytes, long heapMaxBytes,
long uptimeMillis, Instant startTime, String javaVersion, String javaVendor,
String osName, String osArchitecture) { }
public record DatabaseInfo(String status, String product, String version, String versionSource,
String driver, String driverVersion, String storage, String storageSource,
String lifetime, String schemaAction, List<String> warnings) { }
public record DiskInfo(List<String> purposes, Long totalBytes, Long usableBytes, String status) { }
}