This article provides a technical guide on setting up a project to develop a plugin for Graylog, a popular frontend to Elasticsearch that offers document streams, permissions, dashboards, and more.
Since Graylog is written in Java, it is natural that the plugin will be JVM-based. For this guide, we will use Scala. The ultimate goal is to have the message "Hello World!" appear in the Graylog logs during startup. This plugin will not have any user interface components.
- Initialize the SBT project.
- Implement the plugin entry point.
- Implement the HelloWorld service and add it to the plugin.
- Build the plugin JAR.
- Run Graylog with the new plugin.
First, let's create a new project named graylog-plugin-hello-world
using the Scala hello-world
template:
sbt new scala/hello-world.g8
Add dependency on the latest graylog. Next, add the dependency on the latest version of Graylog to the build.sbt file:
libraryDependencies += "org.graylog2" % "graylog2-server" % "5.1.1" % "provided"
Every Graylog plugin must have an entry point. This entry point should be a class that extends the org.graylog2.plugin.Plugin
interface, which has two methods:
PluginMetaData metadata();
Collection<PluginModule> modules();
The metadata()
method should return an object with generic plugin information. The modules()
method should return the Guice module for the plugin, including all necessary initializations.
Let's start by implementing the metadata()
method. Create a new class named HelloWorldMeta
in the com.graylog.plugin.helloworld
package:
package com.graylog.plugin.helloworld
import org.graylog2.plugin.{PluginMetaData, ServerStatus, Version}
import java.net.URI
import java.util
import java.util.Collections
class HelloWorldMeta extends PluginMetaData {
// Set the plugin ID to the meta class name
override def getUniqueId: String = "com.graylog.plugin.helloworld.HelloWorldMeta"
override def getName: String = "Hello World"
override def getAuthor: String = "Vladimir Penkov"
override def getURL: URI = URI.create("https://google.com");
override def getVersion: Version = Version.from(1, 0, 0, "unknown")
override def getDescription: String = "Plugin example"
// Specify the minimum required version
override def getRequiredVersion: Version = Version.from(5, 0, 0, "unknown")
override def getRequiredCapabilities: util.Set[ServerStatus.Capability] = Collections.emptySet()
}
Next, let's create the Guice module for the plugin. Create a new class named HelloWorldModule
in the com.graylog.plugin.helloworld
package:
package com.graylog.plugin.helloworld
class HelloWorldModule extends org.graylog2.plugin.PluginModule {
override def configure(): Unit = {
// Initialize your services here
}
}
Finally, create the plugin entry point. Create a new class named HelloWorldPlugin
in the com.graylog.plugin.helloworld
package::
package com.graylog.plugin.helloworld
import org.graylog2.plugin.{Plugin, PluginMetaData, PluginModule}
import java.util
class HelloWorldPlugin extends Plugin {
override def metadata(): PluginMetaData = new HelloWorldMeta
override def modules(): util.Collection[PluginModule] = util.Collections.singleton(new HelloWorldModule)
}
Now we need to help graylog to find the plugin. Graylog uses ServiceLoader for this.
Create a new file named org.graylog2.plugin.Plugin
in the src/main/resources/META-INF/services
directory and add the following content:
com.graylog.plugin.helloworld.HelloWorldPlugin
Now let's implement the HelloWorldService
that will print "Hello World!" to the Graylog logs during startup.
We will create a service and print the message in it's constructor:
package com.graylog.plugin.helloworld
import org.slf4j.{Logger, LoggerFactory}
class HelloWorldService {
private val log: Logger = LoggerFactory.getLogger(getClass)
log.info("Hello World!")
}
We print the message using slf4j, which comes as a dependency to org.graylog2.graylog2-server
.
Now let's add this service to the guice module:
package com.graylog.plugin.helloworld
class HelloWorldModule extends org.graylog2.plugin.PluginModule {
override def configure(): Unit = {
bind(classOf[HelloWorldService]).asEagerSingleton()
}
}
To build the plugin JAR, run the following command in the project's root directory:
> sbt clean compile package
> ls -la target/scala-2.13/*.jar
-rw-r--r-- 1 penkov staff 5631 May 29 20:47 target/scala-2.13/hello-world_2.13-1.0.jar
The problem at this point is that the jar doesn't contain all scala dependencies. For this we need to create a fat
jar using assembly plugin.
Add this plugin to project/plugins.sbt
(you need to create this file):
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
We need to configure it in build.sbt
:
assembly / assemblyMergeStrategy := {
case PathList("META-INF", xs@_*) =>
xs.map(_.toLowerCase) match {
case ("manifest.mf" :: Nil) | ("notice.txt" :: Nil) | ("index.list" :: Nil) | ("dependencies" :: Nil) =>
MergeStrategy.discard
case ("license.txt" :: Nil) =>
MergeStrategy.discard
case ps@(x :: xs) if ps.last.endsWith(".sf") || ps.last.endsWith(".dsa") =>
MergeStrategy.discard
case "maven" :: xs =>
MergeStrategy.discard
case "pom.properties" :: xs =>
MergeStrategy.discard
case "pom.xml" :: Nil =>
MergeStrategy.discard
case "plexus" :: Nil =>
MergeStrategy.discard
case "io.netty.versions.properties" :: xs =>
MergeStrategy.first
case ("license" :: Nil) | ("notice" :: Nil) =>
MergeStrategy.first
case "services" :: xs =>
MergeStrategy.filterDistinctLines
case ("spring.schemas" :: Nil) | ("spring.handlers" :: Nil) | ("spring.tooling" :: Nil) =>
MergeStrategy.filterDistinctLines
case _ => MergeStrategy.deduplicate
}
case _ => MergeStrategy.first
}
assembly / assemblyJarName := s"${name.value}-${version.value}.jar"
Let's check the jar. Please notice, we use assembly
to build a jar file:
> sbt clean compile assembly
> ls -la target/scala-2.13/*.jar
-rw-r--r-- 1 penkov staff 5993596 May 29 20:52 target/scala-2.13/hello-world-1.0.jar
The size has increased, so everything is correct.
We are almost done. Now we need to actually run the graylog and check it's logs. We will use docker compose for this.
Let's use this docker compose config:
version: '2'
services:
# MongoDB: https://hub.docker.com/_/mongo/
mongodb: # Mongodb service
image: mongo:5 # Version of Mongodb docker image
volumes:
- mongo_data:/data/db # Persisted mongodb data
# https://opensearch.org/docs/latest/install-and-configure/install-opensearch/docker/
opensearch:
image: opensearchproject/opensearch:latest
volumes:
- os_data:/usr/share/opensearch/data # Persisted elasticsearch data
environment:
- bootstrap.memory_lock=true # Disable JVM heap memory swapping
- discovery.type=single-node
- plugins.security.disabled=true
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
ulimits:
memlock:
soft: -1 # Set memlock to unlimited (no soft or hard limit)
hard: -1
nofile:
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
hard: 65536
ports:
- 9200:9200 # REST API
- 9600:9600 # Performance Analyzer
# Graylog: https://hub.docker.com/r/graylog/graylog/
graylog:
image: graylog/graylog:5.1.0 # Version of Graylog docker image
volumes:
- graylog_data:/usr/share/graylog/data # Persisted Graylog data
- ./graylog/plugin/:/usr/share/graylog/plugin
environment:
# CHANGE ME (must be at least 16 characters)!
- GRAYLOG_PASSWORD_SECRET=asdasd12dasasasads332dwqdasdasdqq3
# Password: admin
- GRAYLOG_ROOT_PASSWORD_SHA2=65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5
- GRAYLOG_HTTP_EXTERNAL_URI=http://127.0.0.1:9000/
- GRAYLOG_WEB_ENDPOINT_URI=http://127.0.0.1:9000/api
- GRAYLOG_ELASTICSEARCH_HOSTS=http://opensearch:9200
# Command to run as soon as components are started
entrypoint: /usr/bin/tini -- wait-for-it opensearch:9200 -- /docker-entrypoint.sh
# Containers that Graylog depends on
links:
- mongodb:mongo
- opensearch
restart: always # Graylog container set to always restart when stopped
depends_on:
- mongodb
- opensearch
ports:
# Graylog web interface and REST API
- "9000:9000"
# Syslog TCP
- "1514:1514"
# Syslog UDP
- "1514:1514/udp"
# GELF TCP
- "12201:12201"
# GELF UDP
- "12201:12201/udp"
# Volumes for persisting data, see https://docs.docker.com/engine/admin/volumes/volumes/
volumes:
mongo_data:
driver: local
os_data:
driver: local
graylog_data:
driver: local
GRAYLOG_ROOT_PASSWORD_SHA2
is the hash of the password qwerty
for user admin
, which we will use to login into graylog.
Make sure you use the correct version of the graylog in image: graylog/graylog:5.1.0
.
We mount the graylog/plugin
folder with our plugin as /usr/share/graylog/plugin
. Make sure this folder exists and contains the plugin jar before running graylog:
mkdir -p graylog/plugin
Now let's build our project, copy the output file to the plugins folder and start graylog:
sbt clean compile assembly
cp target/scala-2.13/*.jar graylog/plugin
docker-compose up
You should see in logs:
INFO : com.graylog.plugin.helloworld.HelloWorldService - Hello World!
Congratulations! You have successfully created a basic Graylog plugin that prints "Hello World!" during startup. This serves as a starting point for building more advanced plugins with additional functionality.