diff --git a/pom.xml b/pom.xml
index bdbba283d..2f66ca0b4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,6 +152,11 @@
5.7.1
pom
+
+ net.logstash.logback
+ logstash-logback-encoder
+ 6.6
+
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/LogstashTcpSocketAppenderFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/LogstashTcpSocketAppenderFactory.java
new file mode 100644
index 000000000..d56f65530
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/LogstashTcpSocketAppenderFactory.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.metrics;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.PatternLayout;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
+import ch.qos.logback.core.net.ssl.SSLConfiguration;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.logging.AbstractAppenderFactory;
+import io.dropwizard.logging.async.AsyncAppenderFactory;
+import io.dropwizard.logging.filter.LevelFilterFactory;
+import io.dropwizard.logging.layout.LayoutFactory;
+import java.time.Duration;
+import javax.validation.constraints.NotEmpty;
+import net.logstash.logback.appender.LogstashTcpSocketAppender;
+import net.logstash.logback.encoder.LogstashEncoder;
+
+@JsonTypeName("logstashtcpsocket")
+public class LogstashTcpSocketAppenderFactory extends AbstractAppenderFactory {
+
+ @NotEmpty
+ private String destination;
+
+ private Duration keepAlive = Duration.ofSeconds(20);
+
+ @NotEmpty
+ private String apiKey;
+
+ @JsonProperty
+ public String getDestination() {
+ return destination;
+ }
+
+ @JsonProperty
+ public Duration getKeepAlive() {
+ return keepAlive;
+ }
+
+ @JsonProperty
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ @Override
+ public Appender build(
+ final LoggerContext context,
+ final String applicationName,
+ final LayoutFactory layoutFactory,
+ final LevelFilterFactory levelFilterFactory,
+ final AsyncAppenderFactory asyncAppenderFactory) {
+
+ final SSLConfiguration sslConfiguration = new SSLConfiguration();
+ final LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
+ appender.setName("logstashtcpsocket-appender");
+ appender.setContext(context);
+ appender.setSsl(sslConfiguration);
+ appender.addDestination(destination);
+ appender.setKeepAliveDuration(new ch.qos.logback.core.util.Duration(keepAlive.toMillis()));
+
+ final LogstashEncoder encoder = new LogstashEncoder();
+ final LayoutWrappingEncoder prefix = new LayoutWrappingEncoder<>();
+ final PatternLayout layout = new PatternLayout();
+ layout.setPattern(String.format("%s ", apiKey));
+ prefix.setLayout(layout);
+ encoder.setPrefix(prefix);
+ appender.setEncoder(encoder);
+
+ appender.addFilter(levelFilterFactory.build(threshold));
+ getFilterFactories().forEach(f -> appender.addFilter(f.build()));
+ appender.start();
+
+ return wrapAsync(appender, asyncAppenderFactory);
+ }
+}
diff --git a/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory b/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory
index 48771995f..ebe62c359 100644
--- a/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory
+++ b/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory
@@ -1 +1 @@
-org.whispersystems.textsecuregcm.metrics.LoggingNetworkAppenderFactory
+org.whispersystems.textsecuregcm.metrics.LogstashTcpSocketAppenderFactory