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);
+    }
+
+}