Merge pull request #88 from tkowalcz/87-https-url-config

87 https url config
This commit is contained in:
Tomasz Kowalczewski authored and GitHub committed 2022-05-06 23:23:29 +02:00
commit 6a9e4a4ced
34 files changed
+1141 -41

No files matched your search

@@ -15,8 +15,6 @@ import javax.net.ssl.SSLException;
public class BootstrapUtil {
public static final int HTTPS_PORT = 443;
public static ChannelFuture initConnection(
EventLoopGroup group,
ClientConfiguration clientConfiguration,
@@ -25,7 +23,7 @@ public class BootstrapUtil {
Bootstrap bootstrap = new Bootstrap();
SslContext sslContext = null;
if (isSsl(clientConfiguration)) {
if (clientConfiguration.isUseSSL()) {
sslContext = createSslContext();
}
@@ -49,10 +47,6 @@ public class BootstrapUtil {
).connect();
}
private static boolean isSsl(ClientConfiguration clientConfiguration) {
return clientConfiguration.getPort() == HTTPS_PORT;
}
private static SslContext createSslContext() {
try {
return SslContextBuilder.forClient()
@@ -5,6 +5,7 @@ public class ClientConfiguration {
private final String logEndpoint;
private final String host;
private final int port;
private final boolean useSSL;
private final String username;
private final String password;
@@ -19,6 +20,7 @@ public class ClientConfiguration {
String logEndpoint,
String host,
int port,
boolean useSSL,
String username,
String password,
int connectionTimeoutMillis,
@@ -28,6 +30,7 @@ public class ClientConfiguration {
this.logEndpoint = logEndpoint;
this.host = host;
this.port = port;
this.useSSL = useSSL;
this.username = username;
this.password = password;
@@ -51,6 +54,10 @@ public class ClientConfiguration {
return port;
}
public boolean isUseSSL() {
return useSSL;
}
public String getUsername() {
return username;
}
@@ -1,7 +1,15 @@
package pl.tkowalcz.tjahzi.http;
import java.net.MalformedURLException;
import java.net.URL;
public class ClientConfigurationBuilder {
public static final int HTTPS_PORT = 443;
public static final int HTTP_PORT = 80;
public static final String HTTPS_STRING = "https";
public static final String HTTP_STRING = "http";
public static final String DEFAULT_LOG_ENDPOINT = "/loki/api/v1/push";
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 5_000;
@@ -11,10 +19,15 @@ public class ClientConfigurationBuilder {
public static final int DEFAULT_MAX_RETRIES = 0;
private String logEndpoint = DEFAULT_LOG_ENDPOINT;
private String logEndpoint;
private String url;
private String host;
private int port;
private boolean useSSL;
private String username;
private String password;
@@ -39,6 +52,11 @@ public class ClientConfigurationBuilder {
return this;
}
public ClientConfigurationBuilder withUseSSL(boolean useSSL) {
this.useSSL = useSSL;
return this;
}
public ClientConfigurationBuilder withUsername(String username) {
this.username = username;
return this;
@@ -69,15 +87,29 @@ public class ClientConfigurationBuilder {
return this;
}
public ClientConfigurationBuilder withUrl(String url) {
this.url = url;
return this;
}
public ClientConfiguration build() {
if (maxRequestsInFlight <= 0) {
throw new IllegalArgumentException("Property maxRequestsInFlight must be greater than 0");
}
return new ClientConfiguration(
logEndpoint,
ConnectionParams connectionParams = ConnectionParamsFactory.create(
url,
host,
port,
logEndpoint,
useSSL
);
return new ClientConfiguration(
connectionParams.getLogEndpoint(),
connectionParams.getHost(),
connectionParams.getPort(),
connectionParams.isUseSSL(),
username,
password,
connectionTimeoutMillis,
@@ -0,0 +1,36 @@
package pl.tkowalcz.tjahzi.http;
import java.net.MalformedURLException;
import java.net.URL;
public class ConnectionParams {
private final String host;
private final int port;
private final String logEndpoint;
private final boolean useSSL;
public ConnectionParams(String host, int port, String logEndpoint, boolean useSSL) {
this.host = host;
this.port = port;
this.logEndpoint = logEndpoint;
this.useSSL = useSSL;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getLogEndpoint() {
return logEndpoint;
}
public boolean isUseSSL() {
return useSSL;
}
}
@@ -0,0 +1,110 @@
package pl.tkowalcz.tjahzi.http;
import java.net.MalformedURLException;
import java.net.URL;
public class ConnectionParamsFactory {
public static final int HTTPS_PORT = 443;
public static final int HTTP_PORT = 80;
public static final String HTTPS_STRING = "https";
public static final String HTTP_STRING = "http";
public static final String DEFAULT_LOG_ENDPOINT = "/loki/api/v1/push";
public static ConnectionParams create(String url, String host, int port, String logEndpoint, boolean useSSL) {
if (url != null && host != null) {
throw new IllegalArgumentException("Only one of 'url' or 'host' can be configured. " +
"Current configuration sets url to '" + url + "' and host to '" + host + "'");
}
if (url != null && port != 0) {
throw new IllegalArgumentException("Only one of 'url' or 'port' can be configured. " +
"If using url you must configure port as part of url string. Current configuration sets url to '" + url + "' and port to '" + port + "'");
}
if (url != null && useSSL) {
throw new IllegalArgumentException("Only one of 'url' or 'useSSL' can be configured. " +
"Please use 'https' prefix in url to enable SSL. Current configuration sets url to '" + url + "' and useSSL to '" + useSSL + "'");
}
if (url == null && host == null) {
throw new IllegalArgumentException("One of 'url' or 'host' must be configured.");
}
if (host != null) {
return validateAndConfigureConnectionParametersNoUrl(host, port, logEndpoint, useSSL);
}
return validateAndConfigureConnectionParametersWithUrl(url, logEndpoint, useSSL);
}
private static ConnectionParams validateAndConfigureConnectionParametersNoUrl(String host, int port, String logEndpoint, boolean useSSL) {
if (logEndpoint == null) {
logEndpoint = DEFAULT_LOG_ENDPOINT;
}
if (!useSSL) {
useSSL = port == HTTPS_PORT;
}
return new ConnectionParams(host, port, logEndpoint, useSSL);
}
private static ConnectionParams validateAndConfigureConnectionParametersWithUrl(String url, String logEndpoint, boolean useSSL) {
try {
URL parsedUrl = new URL(url);
String host = parsedUrl.getHost();
int port = configurePort(parsedUrl);
useSSL = validateProtocolSchemeAndConfigureSSL(parsedUrl);
logEndpoint = configureLogEndpoint(parsedUrl, logEndpoint);
return new ConnectionParams(host, port, logEndpoint, useSSL);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Creating configuration with Loki URL failed, url: '" + url + "'", e);
}
}
private static String configureLogEndpoint(URL parsedUrl, String logEndpoint) {
if (logEndpoint != null) {
throw new IllegalArgumentException("Only one of 'url' or 'logEndpoint' can be configured. " +
"Embed log endpoint path into url. " +
"Log endpoint: '" + logEndpoint + "', url: '" + parsedUrl + "'");
}
if (parsedUrl.getPath() != null && !parsedUrl.getPath().isEmpty()) {
return parsedUrl.getPath();
} else {
return DEFAULT_LOG_ENDPOINT;
}
}
private static int configurePort(URL parsedUrl) {
int port = parsedUrl.getPort();
if (port == -1) {
if (HTTPS_STRING.equalsIgnoreCase(parsedUrl.getProtocol())) {
return HTTPS_PORT;
} else {
return HTTP_PORT;
}
}
return port;
}
private static boolean validateProtocolSchemeAndConfigureSSL(URL parsedUrl) {
String protocolScheme = parsedUrl.getProtocol();
if (HTTPS_STRING.equalsIgnoreCase(protocolScheme)) {
return true;
}
if (HTTP_STRING.equalsIgnoreCase(protocolScheme)) {
return false;
}
throw new IllegalArgumentException("Unknown protocol scheme, must be one of 'https' or 'http' (case insensitive). Provided: '" + protocolScheme + "'");
}
}
@@ -0,0 +1,141 @@
package pl.tkowalcz.tjahzi.http;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class ClientConfigurationBuilderTest {
static Stream<Arguments> validConfigOptions() {
return Stream.of(
Arguments.of(
"Should take arguments from url",
"https://example.net:8443/monitoring/loki/api/v1/push",
"example.net",
8443,
"/monitoring/loki/api/v1/push",
true
),
Arguments.of(
"Should not enable ssl if prefix is not https",
"http://example.net:8443/monitoring/loki/api/v1/push",
"example.net",
8443,
"/monitoring/loki/api/v1/push",
false
),
Arguments.of(
"Should use default logEndpoint if none is provided",
"http://example.net:8443",
"example.net",
8443,
"/loki/api/v1/push",
false
),
Arguments.of(
"Should use default http port if none is provided",
"http://example.net",
"example.net",
80,
"/loki/api/v1/push",
false
),
Arguments.of(
"Should use default https port if none is provided",
"https://example.net",
"example.net",
443,
"/loki/api/v1/push",
true
)
);
}
static Stream<Arguments> invalidConfigOptions() {
return Stream.of(
Arguments.of(
"Should reject unknown protocol",
ClientConfiguration.builder().withUrl("file://example.net"),
"Unknown protocol scheme, must be one of 'https' or 'http' (case insensitive). Provided: 'file'"
),
Arguments.of(
"Should not allow url and host",
ClientConfiguration.builder().withUrl("file://example.net").withHost("blah"),
"Only one of 'url' or 'host' can be configured. Current configuration sets url to 'file://example.net' and host to 'blah'"
),
Arguments.of(
"Should not allow url and port",
ClientConfiguration.builder().withUrl("file://example.net").withPort(10),
"Only one of 'url' or 'port' can be configured. If using url you must configure port as part of url string. Current configuration sets url to 'file://example.net' and port to '10'"
),
Arguments.of(
"Should not allow url and useSSL to be defined",
ClientConfiguration.builder().withUrl("file://example.net").withUseSSL(true),
"Only one of 'url' or 'useSSL' can be configured. Please use 'https' prefix in url to enable SSL. Current configuration sets url to 'file://example.net' and useSSL to 'true'"
),
Arguments.of(
"Should require max requests in flight to be positive",
ClientConfiguration.builder().withHost("example.net").withMaxRequestsInFlight(-10),
"Property maxRequestsInFlight must be greater than 0"
),
Arguments.of(
"Should require at least url or host to be specified",
ClientConfiguration.builder(),
"One of 'url' or 'host' must be configured."
),
Arguments.of(
"Should not allow url and logEndpoint to be specified at the same time",
ClientConfiguration.builder().withUrl("http://example.net").withLogEndpoint("blah") ,
"Only one of 'url' or 'logEndpoint' can be configured. Embed log endpoint path into url. Log endpoint: 'blah', url: 'http://example.net'"
)
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("validConfigOptions")
void shouldCorrectlyInterpretUrl(
String testCase,
String url,
String expectedHost,
int expectedPort,
String expectedLogEndpoint,
boolean expectedUseSSL
) {
// Given
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
// When
ClientConfiguration configuration = configurationBuilder
.withUrl(url)
.build();
// Then
assertThat(configuration.getHost()).isEqualTo(expectedHost);
assertThat(configuration.getPort()).isEqualTo(expectedPort);
assertThat(configuration.getLogEndpoint()).isEqualTo(expectedLogEndpoint);
assertThat(configuration.isUseSSL()).isEqualTo(expectedUseSSL);
}
@ParameterizedTest(name = "{0}")
@MethodSource("invalidConfigOptions")
void shouldRejectUnknownProtocolScheme(
String testCase,
ClientConfigurationBuilder clientConfigurationBuilder,
String expectedExceptionMessage
) {
// When
IllegalArgumentException exception = Assertions.assertThrows(
IllegalArgumentException.class,
clientConfigurationBuilder::build
);
// Then
assertThat(exception).hasMessage(expectedExceptionMessage);
}
}
@@ -12,6 +12,7 @@ class HttpHeadersFactoryTest {
void shouldAddAuthHeaderIfCredentialsAreProvided_NoAdditionalHeaders() {
// Given
ClientConfiguration configuration = ClientConfiguration.builder()
.withHost("blah")
.withUsername("foob")
.withPassword("ar")
.build();
@@ -36,6 +37,7 @@ class HttpHeadersFactoryTest {
void shouldAddAuthHeaderIfCredentialsAreProvided_WithAdditionalHeaders() {
// Given
ClientConfiguration configuration = ClientConfiguration.builder()
.withHost("blah")
.withUsername("foob")
.withPassword("ar")
.build();
@@ -65,6 +67,7 @@ class HttpHeadersFactoryTest {
void shouldNotAddAuthHeader_NoAdditionalHeaders() {
// Given
ClientConfiguration configuration = ClientConfiguration.builder()
.withHost("blah")
.build();
String[] additionalHeaders = new String[0];
@@ -84,6 +87,7 @@ class HttpHeadersFactoryTest {
void shouldNOtAddAuthHeader_WithAdditionalHeaders() {
// Given
ClientConfiguration configuration = ClientConfiguration.builder()
.withHost("blah")
.build();
String[] additionalHeaders = new String[]{
+70 -5
View File
@@ -54,7 +54,7 @@ Log4j2 appender seemed like a good first.
### Note on Loki HTTP endpoint and host/port configuration
Tjahzi sends `POST` requests to `/loki/api/v1/push` HTTP endpoint. Specifying
Tjahzi by default sends `POST` requests to `/loki/api/v1/push` HTTP endpoint. Specifying
e.g. `<host>loki.mydomain.com</host><port>3100</port>`
will configure the appender to call to URL: `http://loki.mydomain.com:3100/loki/api/v1/push`.
@@ -114,6 +114,58 @@ attribute of configuration so that the appender can be found.
</configuration>
```
### Configuring connection parameters individually and using URL
At a minimum Tjahzi needs host and port configuration to connect to Loki:
```xml
<host>example.com</host>
<port>3100</port>
```
If port is equal to `443` then SSL will be used. You can also configure SSL manually:
```xml
<host>example.com</host>
<port>3100</port>
<useSSL>true</useSSL>
```
You can also override the default endpoint to which Tjahzi sends data. This can be useful if Loki is behind reverse proxy and
additional path mapping is used:
```xml
<host>example.com</host>
<port>3100</port>
<logEndpoint>/monitoring/loki/api/v1/push</logEndpoint>
```
All these parameters can be configured in one place using URL:
```xml
<url>https://example.com:56654/monitoring/loki/api/v1/push</url>
```
Note that all other tags (host, port, useSSL, logEndpoint) cannot be used when using URL.
| Section | Default | Comment |
|----------|----------------------------|-------------------------------------------------------------------------------------|
| Protocol | None (must be provided) | Supported protocols are `http` and `https`. Https is equvalent to setting `useUSSL` |
| Host | None (must be provided) | |
| Port | 80 for http, 443 for https | You can use any port and SSL will still be used if protocol is set to https |
| Path | '/loki/api/v1/push' | |
Some examples of correct URLs:
```xml
<url>http://example.com</url>
<url>https://example.com:56654</url>
<url>http://example.com/monitoring/loki/api/v1/push</url>
<url>https://example.com:3100/monitoring/foo/bar</url>
```
### Lookups / variable substitution
Contents of the properties
@@ -190,21 +242,34 @@ appender.loki.labels[1].value=log4j
Let's go through the example config used in previous sections and analyze configuration options (**Note: Tags are
case-insensitive**).
#### Host (required)
#### Host (required unless URL is specified)
Network host address of Loki instance. Either IP address or host name. It will be passed to Netty and end up being
resolved by call to `InetSocketAddress.createUnresolved`.
#### Port (required)
#### Port (required unless URL is specified)
Port used for connecting to running Loki. Tjahzi by default uses plain HTTP but if the port is `443` then it will
automatically switch to HTTPS.
#### Username
#### useSSL (optional)
Enable secure (HTTPS) communication regardless of configured port number.
#### logEndpoint (optional)
Overrides the default endpoint to which Tjahzi sends data. This can be useful if Loki is behind reverse proxy and
additional path mapping is used.
#### URL (optional - replaces usage of host, port, useSSL, logEndpoint)
Configure connection in one place instead of using host, port etc. See [this section](#configuring-connection-parameters-individually-and-using-URL).
#### Username (optional)
Username for HTTP basic auth.
#### Password
#### Password (optional)
Password for HTTP basic auth.
+6
View File
@@ -102,6 +102,12 @@
<version>1.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>nginx</artifactId>
<version>1.14.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
@@ -4,7 +4,6 @@ import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
import org.apache.logging.log4j.status.StatusLogger;
import pl.tkowalcz.tjahzi.LoggingSystem;
import pl.tkowalcz.tjahzi.TjahziInitializer;
@@ -37,13 +36,20 @@ public class LokiAppenderBuilder<B extends LokiAppenderBuilder<B>> extends Abstr
private static final int BYTES_IN_KILOBYTE = 1024;
@PluginBuilderAttribute
@Required(message = "No Loki address provided for LokiAppender")
private String url;
@PluginBuilderAttribute
private String logEndpoint;
@PluginBuilderAttribute
private String host;
@PluginBuilderAttribute
@Required(message = "No Loki port provided for LokiAppender")
private int port;
@PluginBuilderAttribute
private boolean useSSL;
@PluginBuilderAttribute
private String username;
@@ -95,8 +101,11 @@ public class LokiAppenderBuilder<B extends LokiAppenderBuilder<B>> extends Abstr
@Override
public LokiAppender build() {
ClientConfiguration configurationBuilder = ClientConfiguration.builder()
.withUrl(url)
.withLogEndpoint(logEndpoint)
.withHost(host)
.withPort(port)
.withUseSSL(useSSL)
.withUsername(username)
.withPassword(password)
.withConnectionTimeoutMillis(connectTimeoutMillis)
@@ -168,6 +177,46 @@ public class LokiAppenderBuilder<B extends LokiAppenderBuilder<B>> extends Abstr
return host;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getLogEndpoint() {
return logEndpoint;
}
public void setLogEndpoint(String logEndpoint) {
this.logEndpoint = logEndpoint;
}
public boolean isUseSSL() {
return useSSL;
}
public void setUseSSL(boolean useSSL) {
this.useSSL = useSSL;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getConnectTimeoutMillis() {
return connectTimeoutMillis;
}
@@ -3,15 +3,13 @@ package pl.tkowalcz.tjahzi.log4j2;
import org.apache.logging.log4j.core.LoggerContext;
import org.junit.jupiter.api.Test;
import java.net.URISyntaxException;
import static org.assertj.core.api.Assertions.assertThat;
import static pl.tkowalcz.tjahzi.log4j2.infra.IntegrationTest.loadConfig;
class BufferSizeConfigurationTest {
@Test
void shouldUseDefaultWhenLogBufferSizeIsNotSetViaConfiguration() throws URISyntaxException {
void shouldUseDefaultWhenLogBufferSizeIsNotSetViaConfiguration() {
// When
LoggerContext context = loadConfig("appender-test-with-buffer-size-megabytes-unset.xml");
@@ -22,7 +20,7 @@ class BufferSizeConfigurationTest {
}
@Test
void shouldSetLogBufferSizeViaConfiguration() throws URISyntaxException {
void shouldSetLogBufferSizeViaConfiguration() {
// When
LoggerContext context = loadConfig("appender-test-with-buffer-size-megabytes-set-to-64.xml");
@@ -0,0 +1,75 @@
package pl.tkowalcz.tjahzi.log4j2;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.NginxContainer;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.junit.jupiter.Container;
import pl.tkowalcz.tjahzi.log4j2.infra.IntegrationTest;
import pl.tkowalcz.tjahzi.log4j2.infra.LokiAssert;
import static org.hamcrest.CoreMatchers.*;
class LokiAppenderHttpsCustomEndpointTest extends IntegrationTest {
@Container
public NginxContainer<?> nginx = new NginxContainer<>("nginx:latest")
.withNetwork(network)
.withClasspathResourceMapping("loki.reverse.nginx.conf",
"/etc/nginx/conf.d/loki.reverse.conf",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.crt",
"/etc/nginx/nginx-selfsigned.crt",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.key",
"/etc/nginx/nginx-selfsigned.key",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/passwords",
"/etc/nginx/passwords",
BindMode.READ_ONLY
)
.waitingFor(new HttpWaitStrategy())
.withExposedPorts(81);
@Override
@BeforeEach
public void setUp() {
System.setProperty("loki.host", nginx.getHost());
System.setProperty("loki.port", nginx.getFirstMappedPort().toString());
}
@Test
void shouldSendData() {
// Given
loadConfig("appender-test-custom-endpoint-configuration.xml");
Logger logger = LogManager.getLogger(LokiAppenderHttpsCustomEndpointTest.class);
String expectedLogLine = "Hello World";
// When
logger.info(expectedLogLine);
// Then
LokiAssert.assertThat(loki)
.returns(response -> response
.body("data.result.size()", equalTo(1))
.body("data.result[0].stream.server", equalTo("127.0.0.1"))
.body(
"data.result.values",
hasItems(
hasItems(
hasItems(
containsString("INFO LokiAppenderHttpsCustomEndpointTest - " + expectedLogLine)
)
)
)
)
);
}
}
@@ -0,0 +1,75 @@
package pl.tkowalcz.tjahzi.log4j2;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.NginxContainer;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.junit.jupiter.Container;
import pl.tkowalcz.tjahzi.log4j2.infra.IntegrationTest;
import pl.tkowalcz.tjahzi.log4j2.infra.LokiAssert;
import static org.hamcrest.CoreMatchers.*;
class LokiAppenderHttpsUrlConnectionReverseProxyTest extends IntegrationTest {
@Container
public NginxContainer<?> nginx = new NginxContainer<>("nginx:latest")
.withNetwork(network)
.withClasspathResourceMapping("loki.reverse.nginx.conf",
"/etc/nginx/conf.d/loki.reverse.conf",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.crt",
"/etc/nginx/nginx-selfsigned.crt",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.key",
"/etc/nginx/nginx-selfsigned.key",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/passwords",
"/etc/nginx/passwords",
BindMode.READ_ONLY
)
.waitingFor(new HttpWaitStrategy())
.withExposedPorts(81);
@Override
@BeforeEach
public void setUp() {
System.setProperty("loki.host", nginx.getHost());
System.setProperty("loki.port", nginx.getFirstMappedPort().toString());
}
@Test
void shouldSendData() {
// Given
loadConfig("appender-test-url-configuration.xml");
Logger logger = LogManager.getLogger(LokiAppenderHttpsUrlConnectionReverseProxyTest.class);
String expectedLogLine = "Hello World";
// When
logger.info(expectedLogLine);
// Then
LokiAssert.assertThat(loki)
.returns(response -> response
.body("data.result.size()", equalTo(1))
.body("data.result[0].stream.server", equalTo("127.0.0.1"))
.body(
"data.result.values",
hasItems(
hasItems(
hasItems(
containsString("INFO LokiAppenderHttpsUrlConnectionReverseProxyTest - " + expectedLogLine)
)
)
)
)
);
}
}
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -17,8 +18,12 @@ import java.net.URL;
@Testcontainers
public class IntegrationTest {
public static Network network = Network.newNetwork();
@Container
public GenericContainer loki = new GenericContainer("grafana/loki:2.3.0")
.withNetwork(network)
.withNetworkAliases("loki")
.withCommand("-config.file=/etc/loki-config.yaml")
.withClasspathResourceMapping("loki-config.yaml",
"/etc/loki-config.yaml",
@@ -31,7 +36,7 @@ public class IntegrationTest {
.withExposedPorts(3100);
@BeforeEach
void setUp() {
public void setUp() {
System.setProperty("loki.host", loki.getHost());
System.setProperty("loki.port", loki.getFirstMappedPort().toString());
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration packages="pl.tkowalcz.tjahzi.log4j2">
<Loggers>
<Root level="OFF">
</Root>
<Logger name="pl.tkowalcz" level="ALL">
<AppenderRef ref="Loki"/>
</Logger>
</Loggers>
<appenders>
<Loki name="Loki">
<host>${sys:loki.host}</host>
<port>${sys:loki.port}</port>
<useSSL>true</useSSL>
<logEndpoint>/monitoring/loki/api/v1/push</logEndpoint>
<username>tjahzi</username>
<password>tjahzi</password>
<ThresholdFilter level="ALL"/>
<PatternLayout>
<Pattern>[%t] %d{DEFAULT} %5p %c{1} - %m%n%exception{full}</Pattern>
</PatternLayout>
<Header name="server" value="127.0.0.1"/>
<Label name="server" value="127.0.0.1"/>
</Loki>
</appenders>
</configuration>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration packages="pl.tkowalcz.tjahzi.log4j2">
<Loggers>
<Root level="OFF">
</Root>
<Logger name="pl.tkowalcz" level="ALL">
<AppenderRef ref="Loki"/>
</Logger>
</Loggers>
<appenders>
<Loki name="Loki">
<url>https://${sys:loki.host}:${sys:loki.port}/monitoring/loki/api/v1/push</url>
<username>tjahzi</username>
<password>tjahzi</password>
<ThresholdFilter level="ALL"/>
<PatternLayout>
<Pattern>[%t] %d{DEFAULT} %5p %c{1} - %m%n%exception{full}</Pattern>
</PatternLayout>
<Header name="server" value="127.0.0.1"/>
<Label name="server" value="127.0.0.1"/>
</Loki>
</appenders>
</configuration>
@@ -0,0 +1,12 @@
server {
listen 81 ssl;
ssl_certificate nginx-selfsigned.crt;
ssl_certificate_key nginx-selfsigned.key;
location /monitoring/ {
proxy_pass http://loki:3100/;
}
auth_basic "loki auth";
auth_basic_user_file /etc/nginx/passwords;
}
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICljCCAX4CCQDDkzDNweN3/jANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJQ
TDAeFw0yMjA1MDMyMjA0MzRaFw0yMzA1MDMyMjA0MzRaMA0xCzAJBgNVBAYTAlBM
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6glTZOdvg9TbDu6fk/L
pZkWFegPALIkCf+ckk+yDSpyg+kuD4XR96NdcxRrMfQBCJGfTBRnpEI4VyZD5aBf
xRoqN6DwRrE8fN7r25C5wi3O7Xewz1NKmVnll+RnjZH84UuC3g+5RMVg7xDM8jrw
pdQnw8EY2bBfT8G+1JLWdFBdQDByblMJv+w0LdisqbV0A/bTdLMD8a2Pw7lbciMC
s0rrdlfcga0QsI7PsraOjVzjl29Cm09YNUjeiR4dD6ZNSAC73qdi1n9OPfrct648
Q/kbR2jiwHzvnQUwj/UCKncs1Vxc84YDaCxiMQYyaHxvaukUFWzE3SLu/OKtpHcE
7wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBBRVyWs2RLG3WJoqx7ArSXAg43uikv
D7Tvo+ZIRuX7GRtQTk5N3nhyG5EVY85j9LX1MlaTkLeuAjHFuWjgRXe6cSFThnrA
qZRMH19jIDlTsd1YOurGrNQCST6R6YJ3rrPrcRDuLow/nO+4hD903DzzuLSjqoWw
TRIRwEcKuX/YrXX81/uFLEonuL97N4HkDnIhPZy/ndS8qAdrH6T47XZ6jh4WYInC
q0FNEmIjbGgTqwOh5wARP83aR6azXpS8Wb6ms2LYBd1BDuoAkqX4Hw5/iHcsqBQ8
UNp7qAH4UQj+cgUL0MQUL07NhJpbcAuAjz6iUXyFv/bb9WR2t2h2qLOu
-----END CERTIFICATE-----
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHqCVNk52+D1Ns
O7p+T8ulmRYV6A8AsiQJ/5yST7INKnKD6S4PhdH3o11zFGsx9AEIkZ9MFGekQjhX
JkPloF/FGio3oPBGsTx83uvbkLnCLc7td7DPU0qZWeWX5GeNkfzhS4LeD7lExWDv
EMzyOvCl1CfDwRjZsF9Pwb7UktZ0UF1AMHJuUwm/7DQt2KyptXQD9tN0swPxrY/D
uVtyIwKzSut2V9yBrRCwjs+yto6NXOOXb0KbT1g1SN6JHh0Ppk1IALvep2LWf049
+ty3rjxD+RtHaOLAfO+dBTCP9QIqdyzVXFzzhgNoLGIxBjJofG9q6RQVbMTdIu78
4q2kdwTvAgMBAAECggEANRmdQjKeY2wEDeccxHdIfDRo0U60nsTCHP6kj60ud53w
cvDTVUGMBEaolfC6ScDRfYvtR/itT9xky5jbufibP/qje8QL42FD6xoGtJCbNu+j
9zg8CAfo8TwdefY/LEHFdc31ckXQSmAMVcisGUhftFrmcIOsrkjgMIAg1rxu2OQG
gLhC5CCESLcGJdHnWnxuQCb81u51b5gftRgoCTRqOBiHf+uopn/cYjCOCCHIwh2I
Dr8yVDcxUIeXzaTnUk3epR4AcYePI8A41KCqIlhQPDy6VbbZmDwHUROMD05ON88z
/PsTUrhAvlAuYXDioxkDXCmU7/4ccUZtg+tVsQe3EQKBgQDmzjimW0RMLKvx5f9V
PTgqrWuQssE1CuDqZAyqianyjkFzE4MteIXVzBh0JreDpKipyUcGu8QnlrS+LCsj
W0DczdIq+WZmTFE8MmMxx5EFxEgXbyidV9P+VeBLgQ5kGYTAOfb6M8oHbSuo9mnQ
6WZDWUwSJ5k5YFFXVjZZp4V1fQKBgQDdc3v1mr9sRwiy5AD641AgeCjmYCMm90j4
QEZr25IgyueT7tA13O/Yi56ggiiQtkN0OjVCqtpF+piWIyEjXprUGf/0c/w16yew
Cfv4j2+DGYqsNrPXU3yBWCOseFm221E/obmvvi8F9aSftTs83dMw9QSYgnnbkmVz
5aPQrZT/2wKBgC3pxhEpxKtWk5naQ/HD7OFogCZXuOGBWX6nCnH+UqVisRhgQE8S
U6VVJWQts4HnhnnIBW4MehfZ/fh6ttYguAV0mCNeW4VsFxjfeSYU6f94rVKkFTM/
nndQw/6hHKjC/spyfXQGS2Ew0WeaJTCezW4GXICcCfoE9AloqAUrsvvNAoGBANV6
4MLxvRtalw4tMqypubKzY2pCv2Bu/s8i10Be2U33jKtViy6BoE22uuKs1wI6UmiX
PITaQ7U2USZbEzUPnF5f+hjkPvyThxzzYw5I6A0P8Fj4GwK3Hs1p9GntUz8tyfN3
qpO62YX7OgokIckZZuBJM3b6QRLk3ByQAg3HaF4FAoGBANUMBR3oUC5Eb7Uzr2oy
xY5mQ9YpN3ePzVUPc1tV8UCILksnJmobL4MFLfH0DWMQpysxBhjKwoYm1UxgYnWK
FtJcYxAybkPvon+cCzOeVcZKQY5Kq5cjBGRKAA79fDQjiFR+mtwVY7JcaDfK8hff
OW7UOrsm/EpNhyG4IYjH8cLd
-----END PRIVATE KEY-----
@@ -0,0 +1 @@
tjahzi:$apr1$7WwaD0YV$Z3WwsmYhmHM9Vsp7euiO21
+71 -14
View File
@@ -111,6 +111,58 @@ class `pl.tkowalcz.tjahzi.logback.LokiAppender`.
</configuration>
```
### Configuring connection parameters individually and using URL
At a minimum Tjahzi needs host and port configuration to connect to Loki:
```xml
<host>example.com</host>
<port>3100</port>
```
If port is equal to `443` then SSL will be used. You can also configure SSL manually:
```xml
<host>example.com</host>
<port>3100</port>
<useSSL>true</useSSL>
```
You can also override the endpoint to which Tjahzi sends data. This can be usefull if Loki is behind reverse proxy and
additional path mapping is used:
```xml
<host>example.com</host>
<port>3100</port>
<logEndpoint>/monitoring/loki/api/v1/push</logEndpoint>
```
All these parameters can be configured in one place using URL:
```xml
<url>https://example.com:56654/monitoring/loki/api/v1/push</url>
```
Note that all other tags (host, port, useSSL, logEndpoint) cannot be used when using URL.
| Section | Default | Comment |
|----------|----------------------------|-------------------------------------------------------------------------------------|
| Protocol | None (must be provided) | Supported protocols are `http` and `https`. Https is equvalent to setting `useUSSL` |
| Host | None (must be provided) | |
| Port | 80 for http, 443 for https | You can use any port and SSL will still be used if protocol is set to https |
| Path | '/loki/api/v1/push' | |
Some examples of correct URLs:
```xml
<url>http://example.com</url>
<url>https://example.com:56654</url>
<url>http://example.com/monitoring/loki/api/v1/push</url>
<url>https://example.com:3100/monitoring/foo/bar</url>
```
### Lookups / variable substitution
Contents of the properties are automatically interpolated by Logback (
@@ -170,6 +222,8 @@ Let's go through the example config above and analyze configuration options (**N
| Port | Self-explanatory :) |
| Encoder | You need to specify pattern for the layout. See section below |
Note: Instead of 'host' and 'port' you can specify URL. See [this section](#configuring-connection-parameters-individually-and-using-URL).
#### Pattern definition
You can define layout and pattern in a traditional Logback way:
@@ -190,17 +244,20 @@ or use Tjahzi optimized efficient, low allocation encoder:
#### Optional configuration parameters
| Tag | Default value | Description |
|-----|---------------|-------------|
| Header | - | This tag can be used multiple times to specify additional headers that are passed to Loki instance. One example is to pass a `X-Scope-OrgID` header when [running Loki in multi-tenant mode](https://grafana.com/docs/loki/latest/operations/authentication/). |
| Label | - | Specify additional labels attached to each log line sent via this appender instance. See also note about [label naming](https://github.com/tkowalcz/tjahzi/wiki/Label-naming). |
| LogLevelLabel | - | If defined then log level label of configured name will be added to each line sent to Loki. It will contain Logback log level e.g. `INFO`, `WARN` etc. See also note about [label naming](https://github.com/tkowalcz/tjahzi/wiki/Label-naming). |
| BufferSizeMegabytes | 32 MB | Size of the `log buffer`. Must be power of two between 1MB and 1GB. See [log buffer sizing](https://github.com/tkowalcz/tjahzi/wiki/Log-buffer-sizing) for more explanations. |
| MaxRetries | 3 | Maximum number of retries to perform when delivering log message to Loki. Log buffer data is delivered in order, one batch after the other, so too much retries will block delivery of subsequent log batches (on the other hand if we need to retry many times then next batches will probably fail too). |
| ConnectTimeoutMillis | 5s | This configures socket connect timeout when connecting to Loki. After unsuccessful connection attempt it will continue to retry indefinitely employing exponential backoff (initial backoff = 250ms, maximum backoff = 30s, multiplier = 3). |
| ReadTimeoutMillis | 60s | Sets socket read timeout on Loki connection. |
| UseOffHeapBuffer | true | Whether Tjahzi should allocate native buffer for `Log buffer` component. We can go into a rabbit hole of divagations what are the implications of this. Most important in our view is that having 10s or 100s of MB of space taken out of heap is not very friendly to garbage collector which might have to occasionally copy it around. |
| BatchSize | 100 KB | Like in [promtail configuration](https://grafana.com/docs/loki/latest/clients/promtail/configuration/) `maximum batch size (in bytes) of logs to accumulate before sending the batch to Loki`.|
| BatchWait | 5s | Like in [promtail configuration](https://grafana.com/docs/loki/latest/clients/promtail/configuration/) `maximum amount of time to wait before sending a batch, even if that batch isn't full`.|
| logShipperWakeupIntervalMillis | 10 | The agent that reads data from log buffer, compresses it and sends to Loki via http is called `LogShipper`. This property controls how often it wakes up to perform its duties. Other properties control how often the data should be sent to Loki (`batchSize`, `batchWait`) this one just control how often to wake up and check for these conditions. In versions before `0.9.17` it was left at default 1ms which caused high CPU usage on some setups. |
| shutdownTimeoutSeconds | 10s | On logging system shutdown (or config reload) Tjahzi will flush its internal buffers so that no logs are lost. This property sets limit on how long to wait for this to complete before proceeding with shutdown. |
| Tag | Default value | Description |
|--------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Url | - | Configure connection in one place instead of using host, port etc. See [this section](#configuring-connection-parameters-individually-and-using-URL). |
| UseSSL | true if port == '443' | Enable secure (HTTPS) communication regardless of configured port number. |
| LogEndpoint | `/loki/api/v1/push` | Overrides the default endpoint to which Tjahzi sends data. This can be useful if Loki is behind reverse proxy and additional path mapping is used. |
| Header | - | This tag can be used multiple times to specify additional headers that are passed to Loki instance. One example is to pass a `X-Scope-OrgID` header when [running Loki in multi-tenant mode](https://grafana.com/docs/loki/latest/operations/authentication/). |
| Label | - | Specify additional labels attached to each log line sent via this appender instance. See also note about [label naming](https://github.com/tkowalcz/tjahzi/wiki/Label-naming). |
| LogLevelLabel | - | If defined then log level label of configured name will be added to each line sent to Loki. It will contain Logback log level e.g. `INFO`, `WARN` etc. See also note about [label naming](https://github.com/tkowalcz/tjahzi/wiki/Label-naming). |
| BufferSizeMegabytes | 32 MB | Size of the `log buffer`. Must be power of two between 1MB and 1GB. See [log buffer sizing](https://github.com/tkowalcz/tjahzi/wiki/Log-buffer-sizing) for more explanations. |
| MaxRetries | 3 | Maximum number of retries to perform when delivering log message to Loki. Log buffer data is delivered in order, one batch after the other, so too much retries will block delivery of subsequent log batches (on the other hand if we need to retry many times then next batches will probably fail too). |
| ConnectTimeoutMillis | 5s | This configures socket connect timeout when connecting to Loki. After unsuccessful connection attempt it will continue to retry indefinitely employing exponential backoff (initial backoff = 250ms, maximum backoff = 30s, multiplier = 3). |
| ReadTimeoutMillis | 60s | Sets socket read timeout on Loki connection. |
| UseOffHeapBuffer | true | Whether Tjahzi should allocate native buffer for `Log buffer` component. We can go into a rabbit hole of divagations what are the implications of this. Most important in our view is that having 10s or 100s of MB of space taken out of heap is not very friendly to garbage collector which might have to occasionally copy it around. |
| BatchSize | 100 KB | Like in [promtail configuration](https://grafana.com/docs/loki/latest/clients/promtail/configuration/) `maximum batch size (in bytes) of logs to accumulate before sending the batch to Loki`. |
| BatchWait | 5s | Like in [promtail configuration](https://grafana.com/docs/loki/latest/clients/promtail/configuration/) `maximum amount of time to wait before sending a batch, even if that batch isn't full`. |
| logShipperWakeupIntervalMillis | 10 | The agent that reads data from log buffer, compresses it and sends to Loki via http is called `LogShipper`. This property controls how often it wakes up to perform its duties. Other properties control how often the data should be sent to Loki (`batchSize`, `batchWait`) this one just control how often to wake up and check for these conditions. In versions before `0.9.17` it was left at default 1ms which caused high CPU usage on some setups. |
| shutdownTimeoutSeconds | 10s | On logging system shutdown (or config reload) Tjahzi will flush its internal buffers so that no logs are lost. This property sets limit on how long to wait for this to complete before proceeding with shutdown. |
+6
View File
@@ -87,6 +87,12 @@
<version>1.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>nginx</artifactId>
<version>1.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -10,9 +10,18 @@ public abstract class LokiAppenderConfigurator extends UnsynchronizedAppenderBas
static final int BYTES_IN_MEGABYTE = 1024 * 1024;
private String url;
private String logEndpoint;
private String host;
private int port;
private boolean useSSL;
private String username;
private String password;
private int connectTimeoutMillis = 5000;
private int readTimeoutMillis = 60_000;
private int maxRetries = 3;
@@ -33,6 +42,22 @@ public abstract class LokiAppenderConfigurator extends UnsynchronizedAppenderBas
private final List<Header> headers = new ArrayList<>();
private final List<Label> labels = new ArrayList<>();
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getLogEndpoint() {
return logEndpoint;
}
public void setLogEndpoint(String logEndpoint) {
this.logEndpoint = logEndpoint;
}
public String getHost() {
return host;
}
@@ -49,6 +74,30 @@ public abstract class LokiAppenderConfigurator extends UnsynchronizedAppenderBas
this.port = port;
}
public boolean isUseSSL() {
return useSSL;
}
public void setUseSSL(boolean useSSL) {
this.useSSL = useSSL;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getConnectTimeoutMillis() {
return connectTimeoutMillis;
}
@@ -40,9 +40,14 @@ public class LokiAppenderFactory {
public LoggingSystem createAppender() {
ClientConfiguration configurationBuilder = ClientConfiguration.builder()
.withUrl(configurator.getUrl())
.withLogEndpoint(configurator.getLogEndpoint())
.withHost(configurator.getHost())
.withConnectionTimeoutMillis(configurator.getConnectTimeoutMillis())
.withPort(configurator.getPort())
.withUseSSL(configurator.isUseSSL())
.withUsername(configurator.getUsername())
.withPassword(configurator.getPassword())
.withConnectionTimeoutMillis(configurator.getConnectTimeoutMillis())
.withMaxRetries(configurator.getMaxRetries())
.withRequestTimeoutMillis(configurator.getReadTimeoutMillis())
.withMaxRequestsInFlight(configurator.getMaxRequestsInFlight())
@@ -10,7 +10,7 @@ import static pl.tkowalcz.tjahzi.logback.infra.IntegrationTest.loadConfig;
class BufferSizeConfigurationTest {
@Test
void shouldUseDefaultWhenLogBufferSizeIsNotSetViaConfiguration() throws Exception {
void shouldUseDefaultWhenLogBufferSizeIsNotSetViaConfiguration() {
// When
LoggerContext context = loadConfig("appender-test-with-buffer-size-megabytes-unset.xml");
@@ -20,7 +20,7 @@ class BufferSizeConfigurationTest {
}
@Test
void shouldSetLogBufferSizeViaConfiguration() throws Exception {
void shouldSetLogBufferSizeViaConfiguration() {
// When
LoggerContext context = loadConfig("appender-test-with-buffer-size-megabytes-set-to-64.xml");
@@ -0,0 +1,75 @@
package pl.tkowalcz.tjahzi.logback;
import ch.qos.logback.classic.LoggerContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.NginxContainer;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.junit.jupiter.Container;
import pl.tkowalcz.tjahzi.logback.infra.IntegrationTest;
import static org.hamcrest.CoreMatchers.*;
import static pl.tkowalcz.tjahzi.logback.infra.LokiAssert.assertThat;
class LokiAppenderHttpsCustomEndpointTest extends IntegrationTest {
@Container
public NginxContainer<?> nginx = new NginxContainer<>("nginx:latest")
.withNetwork(network)
.withClasspathResourceMapping("loki.reverse.nginx.conf",
"/etc/nginx/conf.d/loki.reverse.conf",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.crt",
"/etc/nginx/nginx-selfsigned.crt",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.key",
"/etc/nginx/nginx-selfsigned.key",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/passwords",
"/etc/nginx/passwords",
BindMode.READ_ONLY
)
.waitingFor(new HttpWaitStrategy())
.withExposedPorts(81);
@Override
@BeforeEach
public void setUp() {
System.setProperty("loki.host", nginx.getHost());
System.setProperty("loki.port", nginx.getFirstMappedPort().toString());
}
@Test
void shouldSendData() {
// Given
LoggerContext context = loadConfig("appender-test-custom-endpoint-configuration.xml");
Logger logger = context.getLogger(LokiAppenderHttpsCustomEndpointTest.class);
String expectedLogLine = "Hello World";
// When
logger.info(expectedLogLine);
// Then
assertThat(loki)
.returns(response -> response
.body("data.result.size()", equalTo(1))
.body("data.result[0].stream.server", equalTo("127.0.0.1"))
.body(
"data.result.values",
hasItem(
hasItems(
hasItems(
containsString("p.t.t.l.LokiAppenderHttpsCustomEndpointTest - " + expectedLogLine)
)
)
)
)
);
}
}
@@ -0,0 +1,75 @@
package pl.tkowalcz.tjahzi.logback;
import ch.qos.logback.classic.LoggerContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.NginxContainer;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.junit.jupiter.Container;
import pl.tkowalcz.tjahzi.logback.infra.IntegrationTest;
import static org.hamcrest.CoreMatchers.*;
import static pl.tkowalcz.tjahzi.logback.infra.LokiAssert.assertThat;
class LokiAppenderHttpsUrlConnectionReverseProxyTest extends IntegrationTest {
@Container
public NginxContainer<?> nginx = new NginxContainer<>("nginx:latest")
.withNetwork(network)
.withClasspathResourceMapping("loki.reverse.nginx.conf",
"/etc/nginx/conf.d/loki.reverse.conf",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.crt",
"/etc/nginx/nginx-selfsigned.crt",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/nginx-selfsigned.key",
"/etc/nginx/nginx-selfsigned.key",
BindMode.READ_ONLY
)
.withClasspathResourceMapping("nginx/passwords",
"/etc/nginx/passwords",
BindMode.READ_ONLY
)
.waitingFor(new HttpWaitStrategy())
.withExposedPorts(81);
@Override
@BeforeEach
public void setUp() {
System.setProperty("loki.host", nginx.getHost());
System.setProperty("loki.port", nginx.getFirstMappedPort().toString());
}
@Test
void shouldSendData() {
// Given
LoggerContext context = loadConfig("appender-test-url-configuration.xml");
Logger logger = context.getLogger(LokiAppenderHttpsUrlConnectionReverseProxyTest.class);
String expectedLogLine = "Hello World";
// When
logger.info(expectedLogLine);
// Then
assertThat(loki)
.returns(response -> response
.body("data.result.size()", equalTo(1))
.body("data.result[0].stream.server", equalTo("127.0.0.1"))
.body(
"data.result.values",
hasItem(
hasItems(
hasItems(
containsString("p.t.t.l.LokiAppenderHttpsUrlConnectionReverseProxyTest - " + expectedLogLine)
)
)
)
)
);
}
}
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -19,8 +20,12 @@ import java.net.URISyntaxException;
@Testcontainers
public class IntegrationTest {
public static Network network = Network.newNetwork();
@Container
public GenericContainer loki = new GenericContainer("grafana/loki:2.3.0")
.withNetwork(network)
.withNetworkAliases("loki")
.withCommand("-config.file=/etc/loki-config.yaml")
.withClasspathResourceMapping("loki-config.yaml",
"/etc/loki-config.yaml",
@@ -33,7 +38,7 @@ public class IntegrationTest {
.withExposedPorts(3100);
@BeforeEach
void setUp() {
public void setUp() {
System.setProperty("loki.host", loki.getHost());
System.setProperty("loki.port", loki.getFirstMappedPort().toString());
}
@@ -0,0 +1,30 @@
<configuration debug="true">
<appender name="Loki" class="pl.tkowalcz.tjahzi.logback.LokiAppender">
<host>${loki.host}</host>
<port>${loki.port}</port>
<useSSL>true</useSSL>
<logEndpoint>/monitoring/loki/api/v1/push</logEndpoint>
<username>tjahzi</username>
<password>tjahzi</password>
<efficientLayout>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</efficientLayout>
<header>
<name>X-Org-Id</name>
<value>Circus</value>
</header>
<label>
<name>server</name>
<value>127.0.0.1</value>
</label>
</appender>
<root level="debug">
<appender-ref ref="Loki"/>
</root>
</configuration>
@@ -0,0 +1,26 @@
<configuration debug="true">
<appender name="Loki" class="pl.tkowalcz.tjahzi.logback.LokiAppender">
<url>https://${loki.host}:${loki.port}/monitoring/loki/api/v1/push</url>
<username>tjahzi</username>
<password>tjahzi</password>
<efficientLayout>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</efficientLayout>
<header>
<name>X-Org-Id</name>
<value>Circus</value>
</header>
<label>
<name>server</name>
<value>127.0.0.1</value>
</label>
</appender>
<root level="debug">
<appender-ref ref="Loki"/>
</root>
</configuration>
@@ -0,0 +1,12 @@
server {
listen 81 ssl;
ssl_certificate nginx-selfsigned.crt;
ssl_certificate_key nginx-selfsigned.key;
location /monitoring/ {
proxy_pass http://loki:3100/;
}
auth_basic "loki auth";
auth_basic_user_file /etc/nginx/passwords;
}
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICljCCAX4CCQDDkzDNweN3/jANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJQ
TDAeFw0yMjA1MDMyMjA0MzRaFw0yMzA1MDMyMjA0MzRaMA0xCzAJBgNVBAYTAlBM
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6glTZOdvg9TbDu6fk/L
pZkWFegPALIkCf+ckk+yDSpyg+kuD4XR96NdcxRrMfQBCJGfTBRnpEI4VyZD5aBf
xRoqN6DwRrE8fN7r25C5wi3O7Xewz1NKmVnll+RnjZH84UuC3g+5RMVg7xDM8jrw
pdQnw8EY2bBfT8G+1JLWdFBdQDByblMJv+w0LdisqbV0A/bTdLMD8a2Pw7lbciMC
s0rrdlfcga0QsI7PsraOjVzjl29Cm09YNUjeiR4dD6ZNSAC73qdi1n9OPfrct648
Q/kbR2jiwHzvnQUwj/UCKncs1Vxc84YDaCxiMQYyaHxvaukUFWzE3SLu/OKtpHcE
7wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBBRVyWs2RLG3WJoqx7ArSXAg43uikv
D7Tvo+ZIRuX7GRtQTk5N3nhyG5EVY85j9LX1MlaTkLeuAjHFuWjgRXe6cSFThnrA
qZRMH19jIDlTsd1YOurGrNQCST6R6YJ3rrPrcRDuLow/nO+4hD903DzzuLSjqoWw
TRIRwEcKuX/YrXX81/uFLEonuL97N4HkDnIhPZy/ndS8qAdrH6T47XZ6jh4WYInC
q0FNEmIjbGgTqwOh5wARP83aR6azXpS8Wb6ms2LYBd1BDuoAkqX4Hw5/iHcsqBQ8
UNp7qAH4UQj+cgUL0MQUL07NhJpbcAuAjz6iUXyFv/bb9WR2t2h2qLOu
-----END CERTIFICATE-----
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHqCVNk52+D1Ns
O7p+T8ulmRYV6A8AsiQJ/5yST7INKnKD6S4PhdH3o11zFGsx9AEIkZ9MFGekQjhX
JkPloF/FGio3oPBGsTx83uvbkLnCLc7td7DPU0qZWeWX5GeNkfzhS4LeD7lExWDv
EMzyOvCl1CfDwRjZsF9Pwb7UktZ0UF1AMHJuUwm/7DQt2KyptXQD9tN0swPxrY/D
uVtyIwKzSut2V9yBrRCwjs+yto6NXOOXb0KbT1g1SN6JHh0Ppk1IALvep2LWf049
+ty3rjxD+RtHaOLAfO+dBTCP9QIqdyzVXFzzhgNoLGIxBjJofG9q6RQVbMTdIu78
4q2kdwTvAgMBAAECggEANRmdQjKeY2wEDeccxHdIfDRo0U60nsTCHP6kj60ud53w
cvDTVUGMBEaolfC6ScDRfYvtR/itT9xky5jbufibP/qje8QL42FD6xoGtJCbNu+j
9zg8CAfo8TwdefY/LEHFdc31ckXQSmAMVcisGUhftFrmcIOsrkjgMIAg1rxu2OQG
gLhC5CCESLcGJdHnWnxuQCb81u51b5gftRgoCTRqOBiHf+uopn/cYjCOCCHIwh2I
Dr8yVDcxUIeXzaTnUk3epR4AcYePI8A41KCqIlhQPDy6VbbZmDwHUROMD05ON88z
/PsTUrhAvlAuYXDioxkDXCmU7/4ccUZtg+tVsQe3EQKBgQDmzjimW0RMLKvx5f9V
PTgqrWuQssE1CuDqZAyqianyjkFzE4MteIXVzBh0JreDpKipyUcGu8QnlrS+LCsj
W0DczdIq+WZmTFE8MmMxx5EFxEgXbyidV9P+VeBLgQ5kGYTAOfb6M8oHbSuo9mnQ
6WZDWUwSJ5k5YFFXVjZZp4V1fQKBgQDdc3v1mr9sRwiy5AD641AgeCjmYCMm90j4
QEZr25IgyueT7tA13O/Yi56ggiiQtkN0OjVCqtpF+piWIyEjXprUGf/0c/w16yew
Cfv4j2+DGYqsNrPXU3yBWCOseFm221E/obmvvi8F9aSftTs83dMw9QSYgnnbkmVz
5aPQrZT/2wKBgC3pxhEpxKtWk5naQ/HD7OFogCZXuOGBWX6nCnH+UqVisRhgQE8S
U6VVJWQts4HnhnnIBW4MehfZ/fh6ttYguAV0mCNeW4VsFxjfeSYU6f94rVKkFTM/
nndQw/6hHKjC/spyfXQGS2Ew0WeaJTCezW4GXICcCfoE9AloqAUrsvvNAoGBANV6
4MLxvRtalw4tMqypubKzY2pCv2Bu/s8i10Be2U33jKtViy6BoE22uuKs1wI6UmiX
PITaQ7U2USZbEzUPnF5f+hjkPvyThxzzYw5I6A0P8Fj4GwK3Hs1p9GntUz8tyfN3
qpO62YX7OgokIckZZuBJM3b6QRLk3ByQAg3HaF4FAoGBANUMBR3oUC5Eb7Uzr2oy
xY5mQ9YpN3ePzVUPc1tV8UCILksnJmobL4MFLfH0DWMQpysxBhjKwoYm1UxgYnWK
FtJcYxAybkPvon+cCzOeVcZKQY5Kq5cjBGRKAA79fDQjiFR+mtwVY7JcaDfK8hff
OW7UOrsm/EpNhyG4IYjH8cLd
-----END PRIVATE KEY-----
@@ -0,0 +1 @@
tjahzi:$apr1$7WwaD0YV$Z3WwsmYhmHM9Vsp7euiO21