Log directly to Logstash from Payara Server
Originally published on 30 Nov 2017
Last updated on 30 Nov 2017
(Guest blog)
When running multiple instances of an application server, it is quite hard to see correlations between events. One of the best tools to enable that is the ELK stack - Elasticsearch for building fulltext index of the log entries, Logstash for managing the inflow the events, and Kibana as a user interface on top of that.
Solutions for Payara Server exist, that use better parseable log format which can be then processed by Logstash Filebeat in order to have these log entries processed by a remote Logstash server.
In our project, we chose a different path — we replaced all logging in the server and our applications with Logback, and make use of the logback-logstash-appender
to push the events directly to Logstash over a TCP socket. The appender uses LMAX disruptor internally to push the logs, so the processes does not block the application flow. This article will show you how to have this configured for your project as well.
Log everything via Logstash
In first step, we replace the backend for Payara Server’s logging with Logstash.
Project Goodees was started by me to collect useful small components for Java EE, and Payara Server in particular. The first released component is payara-logback, that includes all libraries properly packaged to use logback as the logging backend in Payara Server and also includes an installation script.
Our logback configuration looks similar to following:
<configuration scan="true">
<jmxConfigurator />
<shutdownHook/>
<!-- propagate changes to java.util.logging --> (1)
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
(2)
<file>${LOG_DIR}/glassfish/${instanceName}/${instanceName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>${LOG_DIR}/glassfish/${instanceName}/${instanceName}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%d{ISO8601}] [%t] [%4p] [%logger]: %m%n</pattern>
</encoder>
</appender>
<appender name="json" class="ch.qos.logback.core.rolling.RollingFileAppender">(3)
<file>${LOG_DIR}/glassfish/${isntanceName}/json/${isntanceName}.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/glassfish/${instanceName}/json/${instanceName}.%d{yyyy-MM-dd}.json.zip</fileNamePattern>
<maxHistory>120</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"instance":"${instanceName}","type":"glassfish"}</customFields>
</encoder>
</appender>
<appender name="logstash-socket" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOGSTASH_DEST}</destination>(4)
<secondaryConnectionTTL>5 minutes</secondaryConnectionTTL>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"instance":"${instanceName}","type":"glassfish"}</customFields>
</encoder>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="FILE" />
<appender-ref ref="json"/>
<appender-ref ref="logstash-socket"/>
</root>
</configuration>
The following points should be noted:
-
Propagation between
slf4j and `java.util.logging
is needed -
We put the logging directory outside of the domain directory, and controlled by an environment variable.
instanceName
is a JVM argument, that is unique for every instance in our system. -
We keep the JSON formatted logs on disk with greater retention period than our Elasticsearch does. If we need to, we can re-upload those to analyze past events.
-
Mutliple destinations in form of
host:port[,host:port]
can be defined for the Logstash socket appender. We keep them in an environment variable.
We have similar configuration for logback-access.xml
:
<configuration scan="true">
<jmxConfigurator/>
<shutdownHook/>
<!-- always a good activate OnConsoleStatusListener -->
<statusListener class="ch.qos.logback.core.status.OnErrorConsoleStatusListener" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/glassfish/${instanceName}/${instanceName}.access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/glassfish/${instanceName}/${instanceName}.access.%d{yyyy-MM-dd}.log.zip</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>combined</pattern>
</encoder>
</appender>
<appender-ref ref="FILE" />
<appender name="json" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/glassfish/${instanceName}/json/${instanceName}.access.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/glassfish/${instanceName}/json/${instanceName}.access.%d{yyyy-MM-dd}.json.zip</fileNamePattern>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.AccessEventCompositeJsonEncoder">
<providers>
<timestamp/>
<version/>
<method/>
<protocol/>
<statusCode/>
<requestedUri/>
<remoteHost/>
<hostname/>
<remoteUser/>
<contentLength/>
<elapsedTime/>
<context/>
<pattern><pattern>
{"instance": "${instanceName}",
"forwarded_for": "%header{X-Forwarded-For}",
"server_name": "%v",
"type": "access"}
</pattern></pattern>
</providers>
</encoder>
</appender>
<appender name="logstash-socket" class="net.logstash.logback.appender.LogstashAccessTcpSocketAppender">
<destination>${LOGSTASH_DEST}</destination>
<secondaryConnectionTTL>5 minutes</secondaryConnectionTTL>
<encoder class="net.logstash.logback.encoder.AccessEventCompositeJsonEncoder">
<providers>
<timestamp/>
<version/>
<method/>
<protocol/>
<statusCode/>
<requestedUri/>
<remoteHost/>
<hostname/>
<remoteUser/>
<contentLength/>
<elapsedTime/>
<context/>
<pattern><pattern>
{"instance": "${instanceName}",
"server_name": "%v",
"forwarded_for": "%header{X-Forwarded-For}",
"type": "access"}
</pattern></pattern>
</providers>
</encoder>
</appender>
<appender-ref ref="logstash-socket" />
<appender-ref ref="json" />
</configuration>
Finally, the Logstash configuration.
It is quite a simple configuration, however access log events tend to have field names that are incompatible with recent versions of elasticsearch. To work around this problem, they need to be adapted a bit, as shown in the filter below:
input {
tcp {
port => 1065
codec => json_lines
}
}
filter {
if [type] == "access" {
mutate {
rename => {
"@fields.HOSTNAME" => "HOSTNAME"
"@fields.content_length" => "content_length"
"@fields.elapsed_time" => "elapsed_time"
"@fields.method" => "method"
"@fields.protocol" => "protocol"
"@fields.remote_host" => "remote_host"
"@fields.remote_user" => "remote_user"
"@fields.requested_uri" => "requested_uri"
"@fields.status_code" => "status_code"
}
}
if [forwarded_for] != "-" {
mutate {
rename => {
"forwarded_for" => "remote_host"
}
}
}
mutate {
remove_field => ["forwarded_for"]
}
}
}
output {
elasticsearch {
hosts => #....
}
}
Adapt your application
Your application also needs small tuning now to prevent various class incompatibility errors. Both slf4j-api
, and logback-classic
should now be provided dependencies of your application. In other words, they should not be included in your .war or .ear build.
Downsides
It is important to note that asadmin set-log-levels
no longer works with this configuration, as logback is the one filtering the levels. You will need a different way of controlling log levels with this kind of configuration.
Give feedback
Let us know how this setup works for you in the comments below. You’re also invited to share your projects' goodees!
Related Posts
The Payara Monthly Catch - November 2024
Published on 28 Nov 2024
by Chiara Civardi
0 Comments
Moving Beyond GlassFish - Here's Why and How
Published on 11 Nov 2024
by Chiara Civardi
0 Comments
If you’re still managing Java applications on GlassFish middleware, you might be hitting some roadblocks, such as difficulties with automation, lack of integrated monitoring and official support and limited abilities with modern tools. However, ...