You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2021/04/22 14:42:57 UTC
[tika] branch branch_1x updated: [TIKA-3353] Prometheus and JMX
monitoring over micrometer (#429)
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch branch_1x
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/branch_1x by this push:
new 64cdc8e [TIKA-3353] Prometheus and JMX monitoring over micrometer (#429)
64cdc8e is described below
commit 64cdc8ef5475d22e55247c667f13eac045366359
Author: Subhajit Das <Su...@users.noreply.github.com>
AuthorDate: Thu Apr 22 20:12:49 2021 +0530
[TIKA-3353] Prometheus and JMX monitoring over micrometer (#429)
* TIKA-3357 removes ambiguity by choosing handler based on produce type
* Removed default compare, as it wil be done by cxf
* Changed priorities
* Added server status metrics
* Added metrics dependencies
* Restructured mbean
* Added metrics for log4j 1.2
* Added licence
* Added metrics general
* Added metrics option
* Added metrics tests
* Fixed log4j metrics and added detailed javadocs.
* Import cleanup
* Improved log4j binding for tika server
---
tika-parent/pom.xml | 1 +
tika-server/pom.xml | 53 +++++
.../java/org/apache/tika/server/TikaServerCli.java | 46 ++---
.../org/apache/tika/server/mbean/MBeanHelper.java | 60 ++++++
.../tika/server/mbean/ServerStatusExporter.java | 1 +
.../server/mbean/ServerStatusExporterMBean.java | 1 +
.../apache/tika/server/metrics/Log4JMetrics.java | 212 ++++++++++++++++++++
.../apache/tika/server/metrics/MetricsHelper.java | 220 +++++++++++++++++++++
.../tika/server/metrics/MetricsResource.java | 53 +++++
.../tika/server/metrics/ServerStatusMetrics.java | 61 ++++++
.../java/org/apache/tika/server/CXFTestBase.java | 12 ++
.../apache/tika/server/MetricsResourceTest.java | 118 +++++++++++
12 files changed, 812 insertions(+), 26 deletions(-)
diff --git a/tika-parent/pom.xml b/tika-parent/pom.xml
index bec3838..7d3cb93 100644
--- a/tika-parent/pom.xml
+++ b/tika-parent/pom.xml
@@ -344,6 +344,7 @@
<cxf.version>3.4.3</cxf.version>
<slf4j.version>1.7.30</slf4j.version>
+ <log4j.version>1.2.17</log4j.version>
<jackson.version>2.12.2</jackson.version>
<!-- when this is next upgraded, see if we can get rid of
javax.activation dependency in tika-server -->
diff --git a/tika-server/pom.xml b/tika-server/pom.xml
index eeb51a4..2bd7d65 100644
--- a/tika-server/pom.xml
+++ b/tika-server/pom.xml
@@ -29,6 +29,8 @@
<url>http://tika.apache.org/</url>
<properties>
+ <cxf.micrometer.version>1.5.12</cxf.micrometer.version>
+ <micrometer-extras.version>0.2.2</micrometer-extras.version>
</properties>
<pluginRepositories>
@@ -164,6 +166,11 @@
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
+ <groupId>log4j</groupId>
+ <artifactId>apache-log4j-extras</artifactId>
+ <version>${log4j.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
@@ -172,6 +179,52 @@
<artifactId>jul-to-slf4j</artifactId>
</dependency>
+ <!-- metrics -->
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-features-metrics</artifactId>
+ <version>${cxf.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>io.dropwizard.metrics</groupId>
+ <artifactId>metrics-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>io.dropwizard.metrics</groupId>
+ <artifactId>metrics-jmx</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.sun.activation</groupId>
+ <artifactId>jakarta.activation</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>io.github.mweirauch</groupId>
+ <artifactId>micrometer-jvm-extras</artifactId>
+ <version>${micrometer-extras.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-core</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-registry-prometheus</artifactId>
+ <version>${cxf.micrometer.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-registry-jmx</artifactId>
+ <version>${cxf.micrometer.version}</version>
+ </dependency>
+
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>tika-core</artifactId>
diff --git a/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java b/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java
index 5167b78..d2d8ba3 100644
--- a/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java
+++ b/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java
@@ -40,6 +40,7 @@ import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.cxf.binding.BindingFactoryManager;
+import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSBindingFactory;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.ResourceProvider;
@@ -52,7 +53,10 @@ import org.apache.tika.config.TikaConfig;
import org.apache.tika.parser.DigestingParser;
import org.apache.tika.parser.utils.BouncyCastleDigester;
import org.apache.tika.parser.utils.CommonsDigester;
+import org.apache.tika.server.mbean.MBeanHelper;
import org.apache.tika.server.mbean.ServerStatusExporter;
+import org.apache.tika.server.metrics.MetricsHelper;
+import org.apache.tika.server.metrics.MetricsResource;
import org.apache.tika.server.resource.DetectorResource;
import org.apache.tika.server.resource.LanguageResource;
import org.apache.tika.server.resource.MetadataResource;
@@ -99,10 +103,10 @@ public class TikaServerCli {
"Please make sure you know what you are doing.";
private static final List<String> ONLY_IN_SPAWN_CHILD_MODE =
- Arrays.asList(new String[] { "taskTimeoutMillis", "taskPulseMillis",
- "pingTimeoutMillis", "pingPulseMillis", "maxFiles", "javaHome", "maxRestarts",
+ Arrays.asList("taskTimeoutMillis", "taskPulseMillis",
+ "pingTimeoutMillis", "pingPulseMillis", "maxFiles", "javaHome", "maxRestarts",
"numRestarts",
- "childStatusFile", "maxChildStartupMillis", "tmpFilePrefix"});
+ "childStatusFile", "maxChildStartupMillis", "tmpFilePrefix");
private static Options getOptions() {
Options options = new Options();
@@ -116,6 +120,7 @@ public class TikaServerCli {
options.addOption("s", "includeStack", false, "whether or not to return a stack trace\nif there is an exception during 'parse'");
options.addOption("i", "id", true, "id to use for server in server status endpoint");
options.addOption("status", false, "enable the status endpoint");
+ options.addOption("metrics", false, "enable metrics collection and expose them");
options.addOption("?", "help", false, "this help message");
options.addOption("enableUnsecureFeatures", false, "this is required to enable fileUrl.");
options.addOption("enableFileUrl", false, "allows user to pass in fileUrl instead of InputStream.");
@@ -324,7 +329,10 @@ public class TikaServerCli {
rCoreProviders.add(new SingletonResourceProvider(new TikaVersion()));
if (line.hasOption("status")) {
rCoreProviders.add(new SingletonResourceProvider(new TikaServerStatus(serverStatus)));
- registerServerStatusMBean(serverStatus);
+ MBeanHelper.registerServerStatusMBean(serverStatus);
+ }
+ if (line.hasOption("metrics")) {
+ rCoreProviders.add(new SingletonResourceProvider(new MetricsResource()));
}
List<ResourceProvider> rAllProviders = new ArrayList<>(rCoreProviders);
rAllProviders.add(new SingletonResourceProvider(new TikaWelcome(rCoreProviders)));
@@ -360,11 +368,18 @@ public class TikaServerCli {
String url = "http://" + host + ":" + port + "/";
sf.setAddress(url);
sf.setResourceComparator(new ProduceTypeResourceComparator());
+ if (line.hasOption("metrics")) {
+ MetricsHelper.initMetrics(sf);
+ MetricsHelper.registerPreStart(serverStatus, line.hasOption("status"));
+ }
BindingFactoryManager manager = sf.getBus().getExtension(BindingFactoryManager.class);
JAXRSBindingFactory factory = new JAXRSBindingFactory();
factory.setBus(sf.getBus());
manager.registerBindingFactory(JAXRSBindingFactory.JAXRS_BINDING_ID, factory);
- sf.create();
+ Server server = sf.create();
+ if (line.hasOption("metrics")) {
+ MetricsHelper.registerPostStart(sf, server);
+ }
LOG.info("Started Apache Tika server at {}", url);
}
@@ -410,25 +425,4 @@ public class TikaServerCli {
return serverTimeouts;
}
- /**
- * Registers MBean server bean for server status (via exporter).
- *
- * @param serverStatus the server status to expose.
- */
- private static void registerServerStatusMBean(ServerStatus serverStatus) {
- try {
- MBeanServer server = ManagementFactory.getPlatformMBeanServer();
- ServerStatusExporter mbean = new ServerStatusExporter(serverStatus);
- final Class<? extends ServerStatusExporter> objectClass = mbean.getClass();
- // Construct the ObjectName for the MBean we will register
- ObjectName mbeanName = new ObjectName(
- String.format(Locale.ROOT, "%s:type=basic,name=%s", objectClass.getPackage().getName(), objectClass.getSimpleName())
- );
- server.registerMBean(mbean, mbeanName);
- LOG.info("Registered Server Status MBean with objectname : {}", mbeanName);
- } catch (Exception e) {
- LOG.warn("Error registering MBean for status", e);
- }
- }
-
}
diff --git a/tika-server/src/main/java/org/apache/tika/server/mbean/MBeanHelper.java b/tika-server/src/main/java/org/apache/tika/server/mbean/MBeanHelper.java
new file mode 100644
index 0000000..e31c932
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/mbean/MBeanHelper.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.tika.server.mbean;
+
+import org.apache.tika.server.ServerStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import java.lang.management.ManagementFactory;
+import java.util.Locale;
+
+/**
+ * Heps setup custom mBeans.
+ */
+public class MBeanHelper {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOG = LoggerFactory.getLogger(MBeanHelper.class);
+
+ /**
+ * Registers MBean server bean for server status (via exporter).
+ *
+ * @param serverStatus the server status to expose.
+ */
+ public static void registerServerStatusMBean(ServerStatus serverStatus) {
+ try {
+ MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+ ServerStatusExporter mbean = new ServerStatusExporter(serverStatus);
+ final Class<? extends ServerStatusExporter> objectClass = mbean.getClass();
+ // Construct the ObjectName for the MBean we will register
+ ObjectName mbeanName = new ObjectName(
+ String.format(Locale.ROOT, "%s:type=basic,name=%s", objectClass.getPackage().getName(), objectClass.getSimpleName())
+ );
+ server.registerMBean(mbean, mbeanName);
+ LOG.info("Registered Server Status MBean with objectname : {}", mbeanName);
+ } catch (Exception e) {
+ LOG.warn("Error registering MBean for status", e);
+ }
+ }
+
+}
diff --git a/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporter.java b/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporter.java
index 00fd33b..3d0d080 100644
--- a/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporter.java
+++ b/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporter.java
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.apache.tika.server.mbean;
import org.apache.tika.server.ServerStatus;
diff --git a/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporterMBean.java b/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporterMBean.java
index 5473b42..3ca32a5 100644
--- a/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporterMBean.java
+++ b/tika-server/src/main/java/org/apache/tika/server/mbean/ServerStatusExporterMBean.java
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.apache.tika.server.mbean;
/**
diff --git a/tika-server/src/main/java/org/apache/tika/server/metrics/Log4JMetrics.java b/tika-server/src/main/java/org/apache/tika/server/metrics/Log4JMetrics.java
new file mode 100644
index 0000000..bb2e1eb
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/metrics/Log4JMetrics.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.tika.server.metrics;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.binder.BaseUnits;
+import io.micrometer.core.instrument.binder.MeterBinder;
+import io.micrometer.core.lang.NonNullApi;
+import io.micrometer.core.lang.NonNullFields;
+import org.apache.commons.collections4.EnumerationUtils;
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.log4j.Appender;
+import org.apache.log4j.AsyncAppender;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.Filter;
+import org.apache.log4j.spi.LoggingEvent;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * Log4J metrics meter binder.
+ */
+public class Log4JMetrics implements MeterBinder, AutoCloseable {
+
+ /**
+ * Te meter name.
+ */
+ private static final String METER_NAME = "log4j.events";
+
+ /**
+ * Additional tags.
+ */
+ private final Iterable<Tag> tags;
+
+ /**
+ * List of appenders that has the metrics filter attached.
+ */
+ private List<Appender> attachedAppenders = new ArrayList<>();
+
+ /**
+ * Initializes metrics with no additional tags.
+ */
+ public Log4JMetrics() {
+ this(Collections.emptyList());
+ }
+
+ /**
+ * Initializes metrics with additional tags.
+ * @param tags the additional tags.
+ */
+ public Log4JMetrics(Iterable<Tag> tags) {
+ this.tags = tags;
+ }
+
+ /**
+ * Binds the metrics to registry.
+ * @param meterRegistry the meter registry to bind to.
+ */
+ @Override
+ public void bindTo(@NotNull MeterRegistry meterRegistry) {
+ Logger rootLogger = LogManager.getRootLogger();
+ attachToAppenders(CastUtils.cast(rootLogger.getAllAppenders(), Appender.class), meterRegistry);
+
+ EnumerationUtils.toList(CastUtils.cast(LogManager.getCurrentLoggers(), Logger.class)).stream()
+ .filter(logger -> !logger.getAdditivity())
+ .forEach(logger -> {
+ if (logger == rootLogger) {
+ return;
+ }
+
+ attachToAppenders(CastUtils.cast(logger.getAllAppenders(), Appender.class), meterRegistry);
+ });
+ }
+
+ /**
+ * Attaches metrics filter to enumeration of appender.
+ * @param appenderEnumeration the appender enumeration.
+ * @param meterRegistry the meter registry to attach to.
+ */
+ private void attachToAppenders(Enumeration<Appender> appenderEnumeration, MeterRegistry meterRegistry) {
+ while (appenderEnumeration.hasMoreElements()) {
+ Appender appender = appenderEnumeration.nextElement();
+
+ if (appender instanceof AsyncAppender) {
+ AsyncAppender asyncAppender = (AsyncAppender) appender;
+ attachToAppenders(CastUtils.cast(asyncAppender.getAllAppenders(), Appender.class), meterRegistry);
+ } else {
+ if (!(appender.getFilter() instanceof MetricsFilter)) {
+ attachMetricsFilter(appender, meterRegistry);
+ }
+ }
+ }
+ }
+
+ /**
+ * Clears all metrics filters.
+ * @throws Exception if an issue.
+ */
+ @Override
+ public void close() throws Exception {
+ for (Appender appender : attachedAppenders) {
+ MetricsFilter metricsFilter = (MetricsFilter) appender.getFilter();
+ appender.clearFilters();
+ appender.addFilter(metricsFilter.getNext());
+ }
+ attachedAppenders.clear();
+ }
+
+ /**
+ * Attaches metrics filter to appender.
+ * @param appender the appender to attach to.
+ * @param meterRegistry the meter registry to bind to.
+ */
+ private void attachMetricsFilter(Appender appender, MeterRegistry meterRegistry) {
+ MetricsFilter metricsFilter = new MetricsFilter(meterRegistry, tags);
+ attachedAppenders.add(appender);
+
+ metricsFilter.setNext(appender.getFilter());
+ appender.clearFilters();
+ appender.addFilter(metricsFilter);
+ }
+
+ /**
+ * Monitors all logging through log4j and keeps count of
+ * all logs for all levels.
+ */
+ @NonNullApi
+ @NonNullFields
+ private class MetricsFilter extends Filter {
+
+ /**
+ * Map of log level string to counter.
+ */
+ private final Map<String, Counter> LEVEL_COUNTER_MAP =
+ new HashMap<>();
+
+ /**
+ * Initializes metrics filter with registry and tags.
+ * @param registry the meter registry to bind to.
+ * @param tags the additional tags.
+ */
+ MetricsFilter(MeterRegistry registry, Iterable<Tag> tags) {
+ Stream.of("fatal", "error", "warn", "info", "debug", "trace")
+ .forEach(levelStr -> LEVEL_COUNTER_MAP.put(levelStr,
+ Counter.builder(METER_NAME)
+ .tags(tags)
+ .tags("level", levelStr)
+ .description("Number of " + levelStr + " level log events")
+ .baseUnit(BaseUnits.EVENTS)
+ .register(registry)));
+ }
+
+ /**
+ * Internally calls the filter chain, and increments based on final decision.
+ * @param event the logging event.
+ * @return the final decision.
+ */
+ @Override
+ public int decide(LoggingEvent event) {
+ int decision = NEUTRAL;
+
+ Filter filter = getNext();
+ while (filter != null) {
+ decision = filter.decide(event);
+ if (decision != NEUTRAL) break;
+ filter = filter.getNext();
+ }
+
+ if (decision != DENY) {
+ incrementCounter(event);
+ }
+
+ return decision;
+ }
+
+ /**
+ * Increments the appropriate counter.
+ * @param event the logging event.
+ */
+ private void incrementCounter(LoggingEvent event) {
+ LEVEL_COUNTER_MAP.get(event.getLevel().toString().toLowerCase(Locale.ROOT))
+ .increment();
+ }
+ }
+
+}
diff --git a/tika-server/src/main/java/org/apache/tika/server/metrics/MetricsHelper.java b/tika-server/src/main/java/org/apache/tika/server/metrics/MetricsHelper.java
new file mode 100644
index 0000000..0ba0ea7
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/metrics/MetricsHelper.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.tika.server.metrics;
+
+import io.github.mweirauch.micrometer.jvm.extras.ProcessMemoryMetrics;
+import io.github.mweirauch.micrometer.jvm.extras.ProcessThreadMetrics;
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.binder.jetty.JettyServerThreadPoolMetrics;
+import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
+import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
+import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
+import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
+import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics;
+import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
+import io.micrometer.core.instrument.binder.system.UptimeMetrics;
+import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
+import io.micrometer.jmx.JmxConfig;
+import io.micrometer.jmx.JmxMeterRegistry;
+import io.micrometer.prometheus.PrometheusConfig;
+import io.micrometer.prometheus.PrometheusMeterRegistry;
+import org.apache.cxf.endpoint.Server;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.metrics.MetricsFeature;
+import org.apache.cxf.metrics.MetricsProvider;
+import org.apache.cxf.metrics.micrometer.MicrometerMetricsProperties;
+import org.apache.cxf.metrics.micrometer.MicrometerMetricsProvider;
+import org.apache.cxf.metrics.micrometer.provider.DefaultExceptionClassProvider;
+import org.apache.cxf.metrics.micrometer.provider.DefaultTimedAnnotationProvider;
+import org.apache.cxf.metrics.micrometer.provider.StandardTags;
+import org.apache.cxf.metrics.micrometer.provider.StandardTagsProvider;
+import org.apache.cxf.metrics.micrometer.provider.TagsCustomizer;
+import org.apache.cxf.metrics.micrometer.provider.TagsProvider;
+import org.apache.cxf.metrics.micrometer.provider.jaxrs.JaxrsOperationTagsCustomizer;
+import org.apache.cxf.metrics.micrometer.provider.jaxrs.JaxrsTags;
+import org.apache.cxf.transport.http_jetty.JettyHTTPServerEngine;
+import org.apache.cxf.transport.http_jetty.JettyHTTPServerEngineFactory;
+import org.apache.tika.server.ServerStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helps setup and configure metrics.
+ */
+public class MetricsHelper {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOG = LoggerFactory.getLogger(MetricsHelper.class);
+
+ /**
+ * Map of Meter registry class to object/instance in use.
+ */
+ private static final Map<Class<? extends MeterRegistry>, MeterRegistry> REGISTRY_MAP
+ = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * The composite registry in use.
+ */
+ private static final CompositeMeterRegistry REGISTRY = Metrics.globalRegistry;
+
+ /**
+ * Gets the sub registry from root registry.
+ * @param tClass the class to get registry of.
+ * @param <T> the registry type.
+ * @return the registry instance.
+ */
+ public static <T extends MeterRegistry> T getRegistry(Class<T> tClass) {
+ return tClass.cast(REGISTRY_MAP.get(tClass));
+ }
+
+ /**
+ * Initialized metrics.
+ * @param sf the server factory bean.
+ */
+ public static void initMetrics(JAXRSServerFactoryBean sf) {
+ final JaxrsTags jaxrsTags = new JaxrsTags();
+ final TagsCustomizer operationsCustomizer = new JaxrsOperationTagsCustomizer(jaxrsTags);
+
+ final TagsProvider tagsProvider = new StandardTagsProvider(new DefaultExceptionClassProvider(),
+ new StandardTags());
+
+ final MicrometerMetricsProperties properties = new MicrometerMetricsProperties();
+ properties.setServerRequestsMetricName("http.server.requests");
+ properties.setServerRequestsMetricName("http.client.requests");
+ properties.setAutoTimeRequests(true);
+
+ final MetricsProvider metricsProvider = new MicrometerMetricsProvider(REGISTRY, tagsProvider,
+ Collections.singletonList(operationsCustomizer),
+ new DefaultTimedAnnotationProvider(), properties);
+
+ setUpRegistries();
+
+ sf.setFeatures(Collections.singletonList(new MetricsFeature(metricsProvider)));
+ }
+
+ /**
+ * Sets up registries.
+ */
+ private static void setUpRegistries() {
+ REGISTRY.config().commonTags("application", "tika-server");
+
+ PrometheusMeterRegistry prometheusMeterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+ REGISTRY_MAP.put(PrometheusMeterRegistry.class, prometheusMeterRegistry);
+ REGISTRY.add(prometheusMeterRegistry);
+
+ JmxMeterRegistry jmxMeterRegistry = new JmxMeterRegistry(JmxConfig.DEFAULT, Clock.SYSTEM);
+ REGISTRY_MAP.put(JmxMeterRegistry.class, jmxMeterRegistry);
+ REGISTRY.add(jmxMeterRegistry);
+ }
+
+ /**
+ * Register meters before start.
+ * @param serverStatus the server status object.
+ * @param enableStatus whether to enable server status metrics.
+ */
+ public static void registerPreStart(ServerStatus serverStatus,
+ boolean enableStatus) {
+
+ if (enableStatus) {
+ setUpServerStatusMetrics(serverStatus);
+ }
+ setUpJvmMetrics();
+ setUpSystemMetrics();
+ setUpLoggingMetrics();
+ setUpExtraMetrics();
+ }
+
+ /**
+ * Sets up server status metrics.
+ * @param serverStatus the server status.
+ */
+ private static void setUpServerStatusMetrics(ServerStatus serverStatus) {
+ new ServerStatusMetrics(serverStatus).bindTo(REGISTRY);
+ }
+
+ /**
+ * Sets up jvm metrics.
+ */
+ private static void setUpJvmMetrics() {
+ new ClassLoaderMetrics().bindTo(REGISTRY);
+ new JvmMemoryMetrics().bindTo(REGISTRY);
+ new JvmGcMetrics().bindTo(REGISTRY);
+ new JvmThreadMetrics().bindTo(REGISTRY);
+ }
+
+ /**
+ * Sets up system level metrics.
+ */
+ private static void setUpSystemMetrics() {
+ new ProcessorMetrics().bindTo(REGISTRY);
+ new FileDescriptorMetrics().bindTo(REGISTRY);
+ new UptimeMetrics().bindTo(REGISTRY);
+ }
+
+ /**
+ * Sets up logging metrics.
+ */
+ private static void setUpLoggingMetrics() {
+ new Log4JMetrics().bindTo(REGISTRY);
+ }
+
+ /**
+ * Sets up jvm extras metrics.
+ */
+ private static void setUpExtraMetrics() {
+ new ProcessThreadMetrics().bindTo(REGISTRY);
+ new ProcessMemoryMetrics().bindTo(REGISTRY);
+ }
+
+ /**
+ * Registers meters post start.
+ * @param sf the server factory bean.
+ * @param server the cxf server.
+ */
+ public static void registerPostStart(JAXRSServerFactoryBean sf, Server server) {
+ setUpJettyThreadPoolMetrics(sf, server);
+ }
+
+ /**
+ * Sets up jetty thread pool metrics.
+ * @param sf the server factory bean.
+ * @param server the cxf server.
+ */
+ private static void setUpJettyThreadPoolMetrics(JAXRSServerFactoryBean sf, Server server) {
+ JettyHTTPServerEngineFactory engineFactory = sf.getBus()
+ .getExtension(JettyHTTPServerEngineFactory.class);
+ JettyHTTPServerEngine engine = engineFactory.retrieveJettyHTTPServerEngine(
+ URI.create(server.getDestination().getAddress().getAddress().getValue())
+ .getPort()
+ );
+ org.eclipse.jetty.server.Server jettyServer = engine.getServer();
+
+ new JettyServerThreadPoolMetrics(jettyServer.getThreadPool(), Collections.emptyList())
+ .bindTo(REGISTRY);
+ }
+
+}
diff --git a/tika-server/src/main/java/org/apache/tika/server/metrics/MetricsResource.java b/tika-server/src/main/java/org/apache/tika/server/metrics/MetricsResource.java
new file mode 100644
index 0000000..4f67697
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/metrics/MetricsResource.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.tika.server.metrics;
+
+import io.micrometer.prometheus.PrometheusMeterRegistry;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * The resource class to serve metrics endpoints.
+ */
+@Path("/metrics")
+public class MetricsResource {
+
+ /**
+ * The default metrics endpoint.
+ * Exports prometheus style metrics.
+ * @return the prometheus format metrics.
+ */
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String scrapePrometheusRegistry() {
+ PrometheusMeterRegistry prometheusMeterRegistry =
+ MetricsHelper.getRegistry(PrometheusMeterRegistry.class);
+ if (prometheusMeterRegistry != null) {
+ return prometheusMeterRegistry.scrape();
+ } else {
+ throw new WebApplicationException("Prometheus exporter not initialized",
+ Response.Status.BAD_REQUEST);
+ }
+ }
+
+}
diff --git a/tika-server/src/main/java/org/apache/tika/server/metrics/ServerStatusMetrics.java b/tika-server/src/main/java/org/apache/tika/server/metrics/ServerStatusMetrics.java
new file mode 100644
index 0000000..50d414a
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/metrics/ServerStatusMetrics.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.tika.server.metrics;
+
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.binder.MeterBinder;
+import org.apache.tika.server.ServerStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Server status metrics meter binder.
+ */
+public class ServerStatusMetrics implements MeterBinder {
+
+ /**
+ * The server status currently in use.
+ */
+ private ServerStatus serverStatus;
+
+ /**
+ * Initializes server status metrics with the server status object.
+ * @param serverStatus the server status.
+ */
+ public ServerStatusMetrics(ServerStatus serverStatus) {
+ this.serverStatus = serverStatus;
+ }
+
+ /**
+ * Binds server status metrics to meter registry.
+ * @param meterRegistry the meter registry to bind to.
+ */
+ @Override
+ public void bindTo(@NotNull MeterRegistry meterRegistry) {
+ Gauge.builder("server.status.lastparsed", serverStatus, ServerStatus::getMillisSinceLastParseStarted)
+ .description("Last parsed in milliseconds")
+ .register(meterRegistry);
+ Gauge.builder("server.status.restarts", serverStatus, ServerStatus::getNumRestarts)
+ .description("Last parsed in milliseconds")
+ .register(meterRegistry);
+ Gauge.builder("server.status.files", serverStatus, ServerStatus::getFilesProcessed)
+ .description("Last parsed in milliseconds")
+ .register(meterRegistry);
+ }
+
+}
diff --git a/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java b/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java
index 0801a25..e9ab560 100644
--- a/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java
+++ b/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java
@@ -107,6 +107,7 @@ public abstract class CXFTestBase {
setUpProviders(sf);
sf.setAddress(endPoint + "/");
sf.setResourceComparator(new ProduceTypeResourceComparator());
+ setUpFeatures(sf);
BindingFactoryManager manager = sf.getBus().getExtension(
BindingFactoryManager.class
);
@@ -119,6 +120,7 @@ public abstract class CXFTestBase {
factory
);
server = sf.create();
+ setUpPostProcess(sf, server);
}
protected boolean isIncludeStackTrace() {
@@ -140,6 +142,16 @@ public abstract class CXFTestBase {
*/
protected abstract void setUpProviders(JAXRSServerFactoryBean sf);
+ /**
+ * Have the test do {@link JAXRSServerFactoryBean#setFeatures(java.util.List)}, if needed
+ */
+ protected void setUpFeatures(JAXRSServerFactoryBean sf) {}
+
+ /**
+ * Have the test do additional steps after it is started, if needed
+ */
+ protected void setUpPostProcess(JAXRSServerFactoryBean sf, Server server) {}
+
@After
public void tearDown() throws Exception {
server.stop();
diff --git a/tika-server/src/test/java/org/apache/tika/server/MetricsResourceTest.java b/tika-server/src/test/java/org/apache/tika/server/MetricsResourceTest.java
new file mode 100644
index 0000000..eacc805
--- /dev/null
+++ b/tika-server/src/test/java/org/apache/tika/server/MetricsResourceTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.tika.server;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.cxf.endpoint.Server;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+import org.apache.tika.server.metrics.MetricsHelper;
+import org.apache.tika.server.metrics.MetricsResource;
+import org.apache.tika.server.writer.TextMessageBodyWriter;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertTrue;
+
+public class MetricsResourceTest extends CXFTestBase {
+
+ private static final String METRICS_PROMETHEUS_PATH = "/metrics";
+
+ @Override
+ protected void setUpResources(JAXRSServerFactoryBean sf) {
+ sf.setResourceClasses(MetricsResource.class);
+ sf.setResourceProvider(MetricsResource.class,
+ new SingletonResourceProvider(new MetricsResource()));
+ }
+
+ @Override
+ protected void setUpProviders(JAXRSServerFactoryBean sf) {
+ List<Object> providers = new ArrayList<>();
+ providers.add(new TextMessageBodyWriter());
+ providers.add(new TikaServerParseExceptionMapper(false));
+ sf.setProviders(providers);
+ }
+
+ @Override
+ protected void setUpFeatures(JAXRSServerFactoryBean sf) {
+ MetricsHelper.initMetrics(sf);
+ MetricsHelper.registerPreStart(null, false);
+ }
+
+ @Override
+ protected void setUpPostProcess(JAXRSServerFactoryBean sf, Server server) {
+ MetricsHelper.registerPostStart(sf, server);
+ }
+
+ @Test
+ public void testPrometheusInitialized() throws IOException {
+ assertContains("jvm_buffer_count", getPrometheusResponse());
+ }
+
+ @Test
+ public void testPrometheusContainsExtras() throws IOException {
+ String response = getPrometheusResponse();
+ assertContains("process_threads", response);
+ assertContains("process_memory", response);
+ }
+
+ @Test
+ public void testPrometheusContainsThreadPool() throws IOException {
+ String response = getPrometheusResponse();
+ assertContains("jetty_threads_config_min", response);
+ assertTrue(getMetricValue(response, "jetty_threads_config_min") > 1.0);
+ assertTrue(getMetricValue(response, "jetty_threads_current") > 1.0);
+ }
+
+ @Test
+ public void testPrometheusContainsLog4j() throws IOException {
+ String response = getPrometheusResponse();
+ assertContains("log4j_events_total", response);
+ assertTrue(getMetricValue(response, "log4j_events_total{application=\"tika-server\",level=\"info\",}") > 1.0);
+ }
+
+ private String getPrometheusResponse() throws IOException {
+ Response response = WebClient.create(endPoint + METRICS_PROMETHEUS_PATH)
+ .type(MediaType.TEXT_PLAIN).accept(MediaType.TEXT_PLAIN).get();
+
+ return getStringFromInputStream((InputStream) response.getEntity());
+ }
+
+ private double getMetricValue(String scrape, String metricName) {
+ return Arrays.stream(scrape.split("\n"))
+ .map(String::trim)
+ .filter(s -> s.startsWith(metricName))
+ .findFirst()
+ .map(s -> Double.valueOf(s.split("\\s+")[s.split("\\s+").length - 1]))
+ .get();
+ }
+
+ static String getStringFromInputStream(InputStream in) throws IOException {
+ return IOUtils.toString(in, UTF_8);
+ }
+
+}