diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index a605ed52f69..83cc7b20340 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -47,7 +47,7 @@ jobs: - name: Run the tests timeout-minutes: 90 # We need a long timeout here env: - MAVEN_OPTS: -Xmx12g -Xms4g -XX:MaxMetaspaceSize=1g + MAVEN_OPTS: -Xmx20g run: mvn --batch-mode --no-transfer-progress --update-snapshots -Dmatchbox.version="${{ env.MATCHBOX_VERSION }}" verify site working-directory: matchbox-int-tests diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6ec99de10bd..6c632c781cf 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -24,10 +24,10 @@ jobs: cache: maven - name: Run the tests in Maven - timeout-minutes: 15 + timeout-minutes: 30 # We need a timeout here env: - MAVEN_OPTS: -Xmx12g + MAVEN_OPTS: -Xmx20g run: mvn --batch-mode --no-transfer-progress --update-snapshots verify - uses: actions/upload-artifact@v4 diff --git a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java index 471e4f2db04..0e7d91a5bdb 100644 --- a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java +++ b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.starter; import ch.ahdis.matchbox.MatchboxRestfulServer; +import ch.ahdis.matchbox.config.MatchboxMetricsConfig; import ch.ahdis.matchbox.config.MatchboxStaticResourceConfig; import ch.ahdis.matchbox.config.MatchboxTxConfig; import ch.ahdis.matchbox.spring.MatchboxEventListener; @@ -39,7 +40,8 @@ RegistryWs.class, MatchboxStaticResourceConfig.class, McpServerConfig.class, - MatchboxTxConfig.class + MatchboxTxConfig.class, + MatchboxMetricsConfig.class }) public class Application extends SpringBootServletInitializer { diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java index 5537f8195ea..aca5266cb14 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java @@ -864,6 +864,10 @@ public int hashCode() { return result; } + public String sessionId() { + return String.valueOf(this.hashCode()); + } + @Override public String toString() { return "CliContext{" + diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java index de7dfefea97..11bda58b37e 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java @@ -57,6 +57,7 @@ import ch.ahdis.matchbox.packages.*; import ch.ahdis.matchbox.providers.*; import ch.ahdis.matchbox.questionnaire.*; +import ch.ahdis.matchbox.util.MatchboxEngineCache; import ch.ahdis.matchbox.util.MatchboxEngineSupport; import ch.ahdis.matchbox.util.MatchboxPackageInstallerImpl; import ch.ahdis.matchbox.validation.ValidationProvider; @@ -242,11 +243,17 @@ public CliContext getCliContext(final Environment environment) { return new CliContext(environment); } + @Bean + public MatchboxEngineCache matchboxEngineCache() { + return new MatchboxEngineCache(); + } + @Bean public MatchboxEngineSupport getMatchboxEngineSupport(final MatchboxFhirContextProperties matchboxFhirContextProperties, final CliContext cliContext, - @Value("${hapi.fhir.fhir_version}") final FhirVersionEnum serverFhirVersion) { - return new MatchboxEngineSupport(matchboxFhirContextProperties, cliContext, serverFhirVersion); + @Value("${hapi.fhir.fhir_version}") final FhirVersionEnum serverFhirVersion, + final MatchboxEngineCache matchboxEngineCache) { + return new MatchboxEngineSupport(matchboxFhirContextProperties, cliContext, serverFhirVersion, matchboxEngineCache); } @Bean diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxMetricsConfig.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxMetricsConfig.java new file mode 100644 index 00000000000..52ac0fefda0 --- /dev/null +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxMetricsConfig.java @@ -0,0 +1,45 @@ +package ch.ahdis.matchbox.config; + +import ch.ahdis.matchbox.packages.MatchboxImplementationGuideProvider; +import ch.ahdis.matchbox.util.MatchboxEngineCache; +import ch.ahdis.matchbox.util.metrics.MatchboxMetrics; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for collecting Matchbox metrics and exposing them via Micrometer. + **/ +@Configuration +public class MatchboxMetricsConfig { + private static final String ENGINE_UNIT = "engines"; + + @Bean + public MeterBinder exposeNumberOfCachedEngines(final MatchboxEngineCache engineCache) { + return registry -> { + Gauge.builder("matchbox.engines.cached.transient.number", engineCache::numberOfTransientEngines) + .description("Number of cached expiring Matchbox engines in the server") + .baseUnit(ENGINE_UNIT) + .register(registry); + Gauge.builder("matchbox.engines.cached.permanent.number", engineCache::numberOfPermanentEngines) + .description("Number of cached immutable Matchbox engines in the server") + .baseUnit(ENGINE_UNIT) + .register(registry); + }; + } + + @Bean + public MeterBinder exposeNumberOfIgs(final MatchboxImplementationGuideProvider implementationGuideProvider) { + return registry -> Gauge.builder("matchbox.igs.number", implementationGuideProvider::count) + .description("Number of installed ImplementationGuides") + .baseUnit("ImplementationGuides") + .register(registry); + } + + @Bean + public MatchboxMetrics matchboxMetrics(final MeterRegistry meterRegistry) { + return new MatchboxMetrics(meterRegistry); + } +} diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/mappinglanguage/StructureMapTransformProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/mappinglanguage/StructureMapTransformProvider.java index 290356e6761..792b5f5c515 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/mappinglanguage/StructureMapTransformProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/mappinglanguage/StructureMapTransformProvider.java @@ -2,9 +2,9 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Set; +import java.util.*; +import ch.ahdis.matchbox.util.metrics.MatchboxMetrics; import jakarta.servlet.ServletOutputStream; /* * #%L @@ -59,8 +59,6 @@ import org.hl7.fhir.r5.model.*; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; /** * StructureMapTransformProvider @@ -70,6 +68,9 @@ public class StructureMapTransformProvider extends StructureMapResourceProvider @Autowired protected MatchboxEngineSupport matchboxEngineSupport; + @Autowired(required = false) + private Optional matchboxMetrics; + private final FhirContext fhirR5Context = FhirContext.forR5Cached(); @Override @@ -97,6 +98,7 @@ public MethodOutcome update(final HttpServletRequest theRequest, @Operation(name = "$transform", type = StructureMap.class, manualResponse = true, manualRequest = true) public void manualInputAndOutput(final HttpServletRequest theServletRequest, final HttpServletResponse theServletResponse) throws IOException { + this.matchboxMetrics.ifPresent(MatchboxMetrics::addTransformation); // Parse the request body, it is either a Parameters resource, or any resource final String body = new String(theServletRequest.getInputStream().readAllBytes()).trim(); @Nullable String resource = null; diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4.java index e10d9a2f566..d6b99d488ad 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4.java @@ -539,4 +539,8 @@ public void installFromInternetRegistry(final String packageId, final String pac .setVersion(packageVersion) ); } + + public long count() { + return this.myPackageVersionDao.count(); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4B.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4B.java index ef01378ac90..ca7fb01ec7b 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4B.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR4B.java @@ -543,4 +543,8 @@ public void installFromInternetRegistry(final String packageId, final String pac .setVersion(packageVersion) ); } + + public long count() { + return this.myPackageVersionDao.count(); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR5.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR5.java index f952615b542..3812026e5ef 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR5.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/ImplementationGuideProviderR5.java @@ -525,4 +525,8 @@ public void installFromInternetRegistry(final String packageId, final String pac .setVersion(packageVersion) ); } + + public long count() { + return this.myPackageVersionDao.count(); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxImplementationGuideProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxImplementationGuideProvider.java index 03253153d77..710177160af 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxImplementationGuideProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxImplementationGuideProvider.java @@ -20,4 +20,9 @@ public interface MatchboxImplementationGuideProvider { * Installs the given ImplementationGuide from the internet registry. */ void installFromInternetRegistry(final String packageId, final String packageVersion); + + /** + * Counts the number of installed ImplementationGuides. + */ + long count(); } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java deleted file mode 100644 index c72b0e5b721..00000000000 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java +++ /dev/null @@ -1,124 +0,0 @@ -/* -* #%L -* Matchbox -* %% -* Copyright (c) 2023- by ahdis ag. All rights reserved. -* %% -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* #L% -*/ - -package ch.ahdis.matchbox.util; - -import java.util.Set; -import java.util.HashSet; -import java.util.Map; - -import org.apache.commons.collections4.map.PassiveExpiringMap; -import org.hl7.fhir.validation.ValidationEngine; -import org.hl7.fhir.validation.service.PassiveExpiringSessionCache; - -/** - * @author Oliver Egger - * - * We want to have a validation engines also that are not timed out as - * in the parent class. - */ -public class EngineSessionCache extends PassiveExpiringSessionCache { - - private final Map cachedSessionsNoTimeout = new java.util.HashMap(); - private final Map cachedSessionIdsNoTimeout = new java.util.HashMap(); - private final PassiveExpiringMap cachedSessionIds; - - final static int TEST_TIME_TO_LIVE = 60; - - - public EngineSessionCache() { - super(TEST_TIME_TO_LIVE,TIME_UNIT); - this.setResetExpirationAfterFetch(true); - cachedSessionIds = new PassiveExpiringMap<>(TEST_TIME_TO_LIVE, TIME_UNIT); - } - - /** - * Returns the stored {@link ValidationEngine} associated with the passed in - * session id, if one such instance exists. - * - * @param sessionId The {@link String} session id. - * @return The {@link ValidationEngine} associated with the passed in id, or - * null if none exists. - */ - @Override - public ValidationEngine fetchSessionValidatorEngine(String sessionId) { - cachedSessionIds.keySet(); // https://github.com/ahdis/matchbox/issues/336 - if (cachedSessionsNoTimeout.containsKey(sessionId)) { - return cachedSessionsNoTimeout.get(sessionId); - } - ValidationEngine valEngine = super.fetchSessionValidatorEngine(sessionId); - if (valEngine!=null && super.resetExpirationAfterFetch) { - cachedSessionIds.put(valEngine, sessionId); - } - return valEngine; - } - - /** - * Returns the set of stored session ids. - * - * @return {@link Set} of session ids. - */ - @Override - public Set getSessionIds() { - Set mergedSet = new HashSet(); - mergedSet.addAll(super.getSessionIds()); - mergedSet.addAll(cachedSessionsNoTimeout.keySet()); - return mergedSet; - } - - /** - * Stores the initialized {@link ValidationEngine} in the cache forever. If a - * null key is - * passed in, a new key is generated and returned. - * - * @param sessionId The {@link String} key to associate with this stored - * {@link ValidationEngine} - * @param validationEngine The {@link ValidationEngine} instance to cache. - * @return The {@link String} id that will be associated with the stored - * {@link ValidationEngine} - */ - public String cacheSessionForEver(String sessionId, ValidationEngine validationEngine) { - cachedSessionsNoTimeout.put(sessionId, validationEngine); - cachedSessionIdsNoTimeout.put(validationEngine, sessionId); - return sessionId; - } - - public String cacheSession(String sessionId, ValidationEngine validationEngine) { - String id = super.cacheSession(sessionId, validationEngine); - cachedSessionIds.put(validationEngine, id); - return sessionId; - } - - @Override - public boolean sessionExists(String sessionId) { - return super.sessionExists(sessionId) || cachedSessionsNoTimeout.containsKey(sessionId); - } - - public String getSessionId(ValidationEngine validationEngine) { - if (cachedSessionIdsNoTimeout.containsKey(validationEngine)) { - return cachedSessionIdsNoTimeout.get(validationEngine); - } - if (cachedSessionIds.containsKey(validationEngine)) { - return cachedSessionIds.get(validationEngine); - } - return null; - } - -} diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineCache.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineCache.java new file mode 100644 index 00000000000..69f8c2dd5e7 --- /dev/null +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineCache.java @@ -0,0 +1,111 @@ +/* + * #%L + * Matchbox + * %% + * Copyright (c) 2023- by ahdis ag. All rights reserved. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +package ch.ahdis.matchbox.util; + +import ch.ahdis.matchbox.CliContext; +import ch.ahdis.matchbox.engine.MatchboxEngine; +import jakarta.annotation.Nullable; +import org.apache.commons.collections4.map.PassiveExpiringMap; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Oliver Egger + *

+ * We want to have a validation engines also that are not timed out as in the parent class. + */ +public class MatchboxEngineCache { + + private final Map permanentEngines = new ConcurrentHashMap<>(8); + private final Map transientEngines = + Collections.synchronizedMap(new PassiveExpiringMap<>(60, TimeUnit.MINUTES)); + + public String cacheTransientEngine(final CliContext cliContext, + final MatchboxEngine engine) { + final String sessionId = cliContext.sessionId(); + this.transientEngines.put(sessionId, engine); + return sessionId; + } + + public String cachePermanentEngine(final CliContext cliContext, + final MatchboxEngine engine) { + final String sessionId = cliContext.sessionId(); + this.permanentEngines.put(sessionId, engine); + return sessionId; + } + + @Nullable + public MatchboxEngine fetchEngine(final String sessionId) { + MatchboxEngine engine = this.permanentEngines.get(sessionId); + if (engine != null) { + return engine; + } + + engine = this.transientEngines.get(sessionId); + if (engine != null) { + // Reset the expiration time of the engine + this.transientEngines.put(sessionId, engine); + return engine; + } + return null; + } + + @Nullable + public MatchboxEngine fetchEngine(final CliContext cliContext) { + return this.fetchEngine(cliContext.sessionId()); + } + + public Set getSessionIds() { + return Stream.of(this.transientEngines.keySet(), this.permanentEngines.keySet()) + .flatMap(Set::stream) + .collect(Collectors.toUnmodifiableSet()); + } + + @Nullable + public String findSessionId(final MatchboxEngine engine) { + for (final Map.Entry entry : this.permanentEngines.entrySet()) { + if (entry.getValue() == engine) { + return entry.getKey(); + } + } + + for (final Map.Entry entry : this.transientEngines.entrySet()) { + if (entry.getValue() == engine) { + return entry.getKey(); + } + } + return null; + } + + public int numberOfTransientEngines() { + return this.transientEngines.size(); + } + + public int numberOfPermanentEngines() { + return this.permanentEngines.size(); + } +} diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java index 8068fcc41fb..277edf8e7d1 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java @@ -51,7 +51,7 @@ public class MatchboxEngineSupport { protected static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MatchboxEngineSupport.class); private static MatchboxEngine mainEngine = null; - private EngineSessionCache sessionCache; + private final MatchboxEngineCache engineCache; private boolean initialized = false; @@ -81,8 +81,9 @@ public class MatchboxEngineSupport { public MatchboxEngineSupport(final MatchboxFhirContextProperties matchboxFhirContextProperties, final CliContext cliContext, - @Value("${hapi.fhir.fhir_version}") final FhirVersionEnum serverFhirVersion) { - this.sessionCache = new EngineSessionCache(); + @Value("${hapi.fhir.fhir_version}") final FhirVersionEnum serverFhirVersion, + final MatchboxEngineCache engineCache) { + this.engineCache = Objects.requireNonNull(engineCache); this.matchboxFhirContextProperties = Objects.requireNonNull(matchboxFhirContextProperties); this.serverFhirVersion = serverFhirVersion; this.cliContext = cliContext; @@ -177,8 +178,8 @@ public NpmPackageVersionResourceEntity loadPackageAssetByLikeCanonicalCurrent(St * @return */ public IBaseResource getCachedResource(final String resource, final @NonNull String id) { - for (final String sessionId : this.sessionCache.getSessionIds()) { - final var engine = (MatchboxEngine) this.sessionCache.fetchSessionValidatorEngine(sessionId); + for (final String sessionId : this.engineCache.getSessionIds()) { + final MatchboxEngine engine = this.engineCache.fetchEngine(sessionId); final IBaseResource res = engine.getCanonicalResourceById(resource, id); if (res != null) { return res; @@ -191,7 +192,7 @@ private MatchboxEngine createMatchboxEngine(final @NonNull MatchboxEngine engine final @Nullable String ig, final @NonNull CliContext cliContext) throws MatchboxEngineCreationException { final String forIg = (ig != null) ? "for " + ig : ""; - log.info("Creating new validate engine {} with parameters {}", forIg, cliContext.hashCode()); + log.info("Creating new validate engine {} with parameters {}", forIg, cliContext.sessionId()); final MatchboxEngine validator; try { validator = new MatchboxEngine(engine); } @@ -220,7 +221,7 @@ private MatchboxEngine createMatchboxEngine(final @NonNull MatchboxEngine engine log.debug("Package Summary: {}", validator.getContext().loadedPackageSummary()); this.configureValidationEngine(validator, cliContext); - log.debug("Finished creating new validate engine for {} with parameters {}", forIg, cliContext.hashCode()); + log.debug("Finished creating new validate engine for {} with parameters {}", forIg, cliContext.sessionId()); return validator; } @@ -353,8 +354,8 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca log.info("Cached default engine forever {} with parameters {}", (cliContextMain.getIg() != null ? "for " + cliContextMain.getIg() : ""), - cliContextMain.hashCode()); - this.sessionCache.cacheSessionForEver("" + cliContextMain.hashCode(), mainEngine); + cliContextMain.sessionId()); + this.engineCache.cachePermanentEngine(cliContextMain, mainEngine); this.cliContext.setIg(null); // otherwise we get for reloads the pacakge name instead a new one later set ahdis/matchbox #144 if (cliContextMain.getIgsPreloaded() != null) { @@ -366,14 +367,14 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca log.error("Error generating matchbox engine due to igLoader", e); } } else { - CliContext cliContextCp = new CliContext(cliContextMain); + final var cliContextCp = new CliContext(cliContextMain); cliContextCp.setIg(ig); // set the ig in the cliContext that hashCode will be - if (this.sessionCache.fetchSessionValidatorEngine("" + cliContextCp.hashCode()) == null) { - MatchboxEngine created = this.createMatchboxEngine(mainEngine, ig, cliContextCp); - this.sessionCache.cacheSessionForEver("" + cliContextCp.hashCode(), created); + if (this.engineCache.fetchEngine(cliContextCp) == null) { + final MatchboxEngine created = this.createMatchboxEngine(mainEngine, ig, cliContextCp); + this.engineCache.cachePermanentEngine(cliContextCp, created); log.info("Cached validate engine forever {} with parameters {}", (ig != null ? "for " + ig : ""), - cliContextCp.hashCode()); + cliContextCp.sessionId()); } } } @@ -422,12 +423,11 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca } // check if we have already a validator in cache for that - final var matchboxEngine = - (MatchboxEngine) this.sessionCache.fetchSessionValidatorEngine("" + cliRequestedContext.hashCode()); + final MatchboxEngine matchboxEngine = this.engineCache.fetchEngine(cliRequestedContext); if (matchboxEngine != null && !reload) { log.info("Using cached validate engine {} with parameters {}", (cliRequestedContext.getIg() != null ? "for " + cliRequestedContext.getIg() : ""), - cliRequestedContext.hashCode()); + cliRequestedContext.sessionId()); // Runtime runtime = Runtime.getRuntime(); // runtime.gc(); return matchboxEngine; @@ -437,7 +437,7 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca if (create && cliRequestedContext.getIg() != null) { log.info("Creating new cached validate engine {} with parameters {}", (cliRequestedContext.getIg() != null ? "for " + cliRequestedContext.getIg() : ""), - cliRequestedContext.hashCode()); + cliRequestedContext.sessionId()); MatchboxEngine baseEngine = mainEngine; if (!cliRequestedContext.getFhirVersion().equals(baseEngine.getVersion())) { log.debug("Creating base engine for {} with parameters and fhir Version {}", @@ -465,7 +465,7 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca } } final var created = this.createMatchboxEngine(baseEngine, cliRequestedContext.getIg(), cliRequestedContext); - this.sessionCache.cacheSession("" + cliRequestedContext.hashCode(), created); + this.engineCache.cacheTransientEngine(cliRequestedContext, created); // Runtime runtime = Runtime.getRuntime(); // runtime.gc(); return created; @@ -474,7 +474,7 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca } public String getSessionId(final MatchboxEngine engine) { - return this.sessionCache.getSessionId(engine); + return this.engineCache.findSessionId(engine); } public boolean isInitialized() { diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/metrics/MatchboxMetrics.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/metrics/MatchboxMetrics.java new file mode 100644 index 00000000000..8da6960beae --- /dev/null +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/metrics/MatchboxMetrics.java @@ -0,0 +1,47 @@ +package ch.ahdis.matchbox.util.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +import java.time.Duration; + +/** + * The class holding different metrics that Matchbox may collect. + * + * @see ch.ahdis.matchbox.config.MatchboxMetricsConfig + **/ +public class MatchboxMetrics { + + private final Counter validationCounter; + private final Timer validationDurationTimer; + private final Counter transformationCounter; + + public MatchboxMetrics(final MeterRegistry meterRegistry) { + this.validationCounter = Counter.builder("matchbox.validation.count") + .description("Number of FHIR resources validated by Matchbox") + .baseUnit("validations") + .register(meterRegistry); + this.validationDurationTimer = Timer.builder("matchbox.validation.duration") + .description("Duration of FHIR resource validation by Matchbox") + .distributionStatisticExpiry(Duration.ofDays(30)) + .distributionStatisticBufferLength(1000) + .register(meterRegistry); + this.transformationCounter = Counter.builder("matchbox.transformation.count") + .description("Number of FHIR resources transformed by Matchbox") + .baseUnit("transformations") + .register(meterRegistry); + } + + public void addValidation() { + this.validationCounter.increment(); + } + + public void addValidationDuration(final Duration duration) { + this.validationDurationTimer.record(duration); + } + + public void addTransformation() { + this.transformationCounter.increment(); + } +} diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java index 9c18b0d2896..e101b6cdf5d 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java @@ -29,6 +29,7 @@ import ch.ahdis.matchbox.CliContext; import ch.ahdis.matchbox.config.MatchboxFhirVersion; import ch.ahdis.matchbox.util.MatchboxEngineSupport; +import ch.ahdis.matchbox.util.metrics.MatchboxMetrics; import ch.ahdis.matchbox.validation.matchspark.LLMConnector; import ch.ahdis.matchbox.engine.MatchboxEngine; import ch.ahdis.matchbox.engine.cli.VersionUtil; @@ -38,14 +39,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; import org.hl7.fhir.r5.extensions.ExtensionDefinitions; -import org.hl7.fhir.r5.model.Duration; -import org.hl7.fhir.r5.model.OperationOutcome; -import org.hl7.fhir.r5.model.StringType; import org.hl7.fhir.r5.utils.EOperationOutcome; -import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.utils.OperationOutcomeUtilities; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.springframework.beans.factory.annotation.Autowired; @@ -60,6 +57,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import static ch.ahdis.matchbox.util.MatchboxServerUtils.addExtension; @@ -90,6 +88,9 @@ public class ValidationProvider { @Autowired private PlatformTransactionManager myTxManager; + @Autowired(required = false) + private Optional matchboxMetrics; + // @Operation(name = "$canonical", manualRequest = true, idempotent = true, returnParameters = { // @OperationParam(name = "return", type = IBase.class, min = 1, max = 1) }) // public IBaseResource canonical(HttpServletRequest theRequest) { @@ -114,6 +115,7 @@ public class ValidationProvider { @OperationParam(name = "return", type = IBase.class, min = 1, max = 1)}) public IBaseResource validate(final HttpServletRequest theRequest) { log.debug("$validate"); + this.matchboxMetrics.ifPresent(MatchboxMetrics::addValidation); final var sw = new StopWatch(); sw.startTask("Total"); @@ -222,6 +224,7 @@ public IBaseResource validate(final HttpServletRequest theRequest) { long millis = sw.getMillis(); log.debug("Validation time: {}", sw); + this.matchboxMetrics.ifPresent(m -> m.addValidationDuration(java.time.Duration.ofMillis(millis))); var oo = this.getOperationOutcome(sha3Hex, messages, profile, engine, millis, cliContext); diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java index b8be5d8e73d..9184a9b0f11 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.util.StopWatch; +import ch.ahdis.matchbox.util.metrics.MatchboxMetrics; import ch.ahdis.matchbox.validation.ValidationProvider; import ch.ahdis.matchbox.CliContext; import ch.ahdis.matchbox.util.MatchboxEngineSupport; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import static ch.ahdis.matchbox.packages.MatchboxJpaPackageCache.structureDefinitionIsValidatable; @@ -50,18 +52,20 @@ public class GazelleValidationWs { private static final String VALIDATE_PATH = "/validation/validate"; private final MatchboxEngineSupport matchboxEngineSupport; - private final StructureDefinitionResourceProvider structureDefinitionProvider; + private final Optional matchboxMetrics; // The base CLI context, with the default parameters private final CliContext baseCliContext; public GazelleValidationWs(final MatchboxEngineSupport matchboxEngineSupport, final CliContext baseCliContext, - final StructureDefinitionResourceProvider structureDefinitionProvider) { + final StructureDefinitionResourceProvider structureDefinitionProvider, + final Optional matchboxMetrics) { this.matchboxEngineSupport = Objects.requireNonNull(matchboxEngineSupport); this.baseCliContext = Objects.requireNonNull(baseCliContext); this.structureDefinitionProvider = Objects.requireNonNull(structureDefinitionProvider); + this.matchboxMetrics = Objects.requireNonNull(matchboxMetrics); } /** @@ -133,6 +137,7 @@ public List getProfiles() { @PostMapping(path = VALIDATE_PATH, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ValidationReport postValidate(@RequestBody final ValidationRequest validationRequest) { + this.matchboxMetrics.ifPresent(MatchboxMetrics::addValidation); final var sw = new StopWatch(); sw.startTask("Total"); diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/GazelleApiR4Test.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/GazelleApiR4Test.java index 7b68dcc6a65..2889377dbb8 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/GazelleApiR4Test.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/GazelleApiR4Test.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -25,7 +24,6 @@ @ContextConfiguration(classes = { Application.class }) @ActiveProfiles("test-r4") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class GazelleApiR4Test extends AbstractGazelleTest { private final GazelleClient client = new GazelleClient("http://localhost:8081/matchboxv3/gazelle/"); diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/mapping/TransformTest.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/mapping/TransformTest.java index 26daec481d7..7f302672df0 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/mapping/TransformTest.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/mapping/TransformTest.java @@ -1,7 +1,6 @@ package ch.ahdis.matchbox.mapping; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.starter.Application; import ch.ahdis.matchbox.test.CompareUtil; import ch.ahdis.matchbox.test.ValidationClient; @@ -12,7 +11,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -36,7 +34,6 @@ @ContextConfiguration(classes = {Application.class}) @ActiveProfiles("test-transform") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class TransformTest { private static final String TARGET_SERVER = "http://localhost:8086/matchboxv3"; private static final FhirContext FHIR_CONTEXT = FhirContext.forR4Cached(); diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4BTest.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4BTest.java index 35635f25179..15b1ddddf59 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4BTest.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4BTest.java @@ -2,7 +2,6 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.starter.Application; @@ -18,7 +17,6 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -37,7 +35,6 @@ @ContextConfiguration(classes = {Application.class}) @ActiveProfiles("test-r4b") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class MatchboxApiR4BTest { static public int getValidationFailures(OperationOutcome outcome) { diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4Test.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4Test.java index 82378a0b370..7e385ab8d3b 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4Test.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR4Test.java @@ -14,15 +14,11 @@ import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r4.model.Parameters; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -42,7 +38,6 @@ @ContextConfiguration(classes = {Application.class}) @ActiveProfiles("test-r4") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) class MatchboxApiR4Test { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MatchboxApiR4Test.class); private static final String TARGET_SERVER = "http://localhost:8081/matchboxv3"; diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5Test.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5Test.java index b240e6709b0..fd44c61d2ed 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5Test.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5Test.java @@ -2,7 +2,6 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.starter.Application; @@ -18,18 +17,13 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; -import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,7 +35,6 @@ @ContextConfiguration(classes = {Application.class}) @ActiveProfiles("test-r5") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class MatchboxApiR5Test { static public int getValidationFailures(OperationOutcome outcome) { diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5onR4Test.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5onR4Test.java index 8a2990c6d97..372c9606b68 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5onR4Test.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiR5onR4Test.java @@ -17,7 +17,6 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -32,7 +31,6 @@ @ContextConfiguration(classes = { Application.class }) @ActiveProfiles("test-r5onr4") @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class MatchboxApiR5onR4Test { static public int getValidationFailures(OperationOutcome outcome) {