diff --git a/data-node/pom.xml b/data-node/pom.xml
index 0ab443497e08..9e20ab3ba221 100644
--- a/data-node/pom.xml
+++ b/data-node/pom.xml
@@ -671,6 +671,34 @@
exec-maven-pluginorg.codehaus.mojo
+
+ generate-csv-docs
+ prepare-package
+
+ java
+
+
+ org.graylog.datanode.docs.ConfigurationDocsGenerator
+
+ csv
+ ${project.build.directory}/datanode-conf-docs.csv
+
+
+
+
+ generate-conf-example
+ prepare-package
+
+ java
+
+
+ org.graylog.datanode.docs.ConfigurationDocsGenerator
+
+ conf
+ ${project.build.directory}/datanode.conf.example
+
+
+ install-required-opensearch-plugins-x64prepare-package
diff --git a/data-node/src/main/assembly/datanode.xml b/data-node/src/main/assembly/datanode.xml
index 8c2dc9fca0e7..d6a1757f4398 100644
--- a/data-node/src/main/assembly/datanode.xml
+++ b/data-node/src/main/assembly/datanode.xml
@@ -75,7 +75,7 @@
.
-
+
datanode.conf.example.
diff --git a/data-node/src/main/java/org/graylog/datanode/Configuration.java b/data-node/src/main/java/org/graylog/datanode/Configuration.java
index 91065ce5dcfb..d9ce175f05e6 100644
--- a/data-node/src/main/java/org/graylog/datanode/Configuration.java
+++ b/data-node/src/main/java/org/graylog/datanode/Configuration.java
@@ -32,8 +32,10 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.InetAddresses;
import org.graylog.datanode.configuration.DatanodeDirectories;
+import org.graylog.datanode.docs.DocumentationConstants;
import org.graylog2.Configuration.SafeClassesValidator;
import org.graylog2.configuration.Documentation;
+import org.graylog2.configuration.DocumentationSection;
import org.graylog2.plugin.Tools;
import org.graylog2.shared.SuppressForbidden;
import org.slf4j.Logger;
@@ -57,7 +59,9 @@
* Helper class to hold configuration of DataNode
*/
@SuppressWarnings("FieldMayBeFinal")
+@DocumentationSection(heading = "GRAYLOG DATANODE CONFIGURATION FILE", description = DocumentationConstants.DATANODE_DOCUMENTATION_DESCRIPTION)
public class Configuration {
+
private static final Logger LOG = LoggerFactory.getLogger(Configuration.class);
public static final String TRANSPORT_CERTIFICATE_PASSWORD_PROPERTY = "transport_certificate_password";
public static final String HTTP_CERTIFICATE_PASSWORD_PROPERTY = "http_certificate_password";
@@ -86,8 +90,10 @@ public class Configuration {
@Parameter(value = "opensearch_location")
private String opensearchDistributionRoot = "dist";
- @Documentation("Data directory of the embedded opensearch. Contains indices of the opensearch. May be pointed to an existing" +
- "opensearch directory during in-place migration to Datanode")
+ @Documentation("""
+ Data directory of the embedded opensearch. Contains indices of the opensearch.
+ May be pointed to an existing opensearch directory during in-place migration to Datanode
+ """)
@Parameter(value = "opensearch_data_location", required = true)
private Path opensearchDataLocation = Path.of("datanode/data");
@@ -95,8 +101,11 @@ public class Configuration {
@Parameter(value = "opensearch_logs_location", required = true, validators = DirectoryWritableValidator.class)
private Path opensearchLogsLocation = Path.of("datanode/logs");
- @Documentation("Configuration directory of the embedded opensearch. This is the directory where the opensearch" +
- "process will store its configuration files. Caution, each start of the Datanode will regenerate the complete content of the directory!")
+ @Documentation("""
+ Configuration directory of the embedded opensearch. This is the directory where the opensearch
+ process will store its configuration files. Caution, each start of the Datanode will regenerate
+ the complete content of the directory!
+ """)
@Parameter(value = "opensearch_config_location", required = true, validators = DirectoryWritableValidator.class)
private Path opensearchConfigLocation = Path.of("datanode/config");
@@ -113,7 +122,10 @@ public class Configuration {
private Integer opensearchProcessLogsBufferSize = 500;
- @Documentation("Unique name of this Datanode instance. use this, if your node name should be different from the hostname that's found by programmatically looking it up")
+ @Documentation("""
+ Unique name of this Datanode instance. use this, if your node name should be different from the hostname
+ that's found by programmatically looking it up.
+ """)
@Parameter(value = "node_name")
private String datanodeNodeName;
@@ -122,7 +134,10 @@ public class Configuration {
@Parameter(value = "initial_cluster_manager_nodes")
private String initialClusterManagerNodes;
- @Documentation("Opensearch heap memory. Initial and maxmium heap must be identical for OpenSearch, otherwise the boot fails. So it's only one config option")
+ @Documentation("""
+ Opensearch heap memory. Initial and maxmium heap must be identical for OpenSearch, otherwise the boot fails.
+ So it's only one config option.
+ """)
@Parameter(value = "opensearch_heap")
private String opensearchHeap = "1g";
@@ -138,7 +153,10 @@ public class Configuration {
@Parameter(value = "opensearch_discovery_seed_hosts", converter = StringListConverter.class)
private List opensearchDiscoverySeedHosts = Collections.emptyList();
- @Documentation("Binds an OpenSearch node to an address. Use 0.0.0.0 to include all available network interfaces, or specify an IP address assigned to a specific interface. ")
+ @Documentation("""
+ Binds an OpenSearch node to an address. Use 0.0.0.0 to include all available network interfaces,
+ or specify an IP address assigned to a specific interface.
+ """)
@Parameter(value = "opensearch_network_host")
private String opensearchNetworkHost = null;
@@ -158,23 +176,36 @@ public class Configuration {
@Parameter(value = HTTP_CERTIFICATE_PASSWORD_PROPERTY)
private String datanodeHttpCertificatePassword;
- @Documentation("You MUST set a secret to secure/pepper the stored user passwords here. Use at least 16 characters." +
- "Generate one by using for example: pwgen -N 1 -s 96 \n" +
- "ATTENTION: This value must be the same on all Graylog and Datanode nodes in the cluster. " +
- "Changing this value after installation will render all user sessions and encrypted values in the database invalid. (e.g. encrypted access tokens)")
+ @Documentation("""
+ You MUST set a secret to secure/pepper the stored user passwords here. Use at least 16 characters.
+ Generate one by using for example: pwgen -N 1 -s 96
+ ATTENTION: This value must be the same on all Graylog and Datanode nodes in the cluster.
+ Changing this value after installation will render all user sessions and encrypted values
+ in the database invalid. (e.g. encrypted access tokens)
+ """)
@Parameter(value = "password_secret", required = true, validators = StringNotBlankValidator.class)
private String passwordSecret;
- @Documentation("communication between Graylog and OpenSearch is secured by JWT. This configuration defines interval between token regenerations.")
+ @DocumentationSection(heading = "OpenSearch JWT token usage",description = """
+ communication between Graylog and OpenSearch is secured by JWT. These are the defaults used for the token usage
+ adjust them, if you have special needs.
+ """)
+ @Documentation(value = "This configuration defines interval between token regenerations.")
@Parameter(value = "indexer_jwt_auth_token_caching_duration")
Duration indexerJwtAuthTokenCachingDuration = Duration.seconds(60);
- @Documentation("communication between Graylog and OpenSearch is secured by JWT. This configuration defines validity interval of JWT tokens.")
+ @DocumentationSection(heading = "OpenSearch JWT token usage",description = """
+ communication between Graylog and OpenSearch is secured by JWT. These are the defaults used for the token usage
+ adjust them, if you have special needs.
+ """)
+ @Documentation("This configuration defines validity interval of JWT tokens")
@Parameter(value = "indexer_jwt_auth_token_expiration_duration")
Duration indexerJwtAuthTokenExpirationDuration = Duration.seconds(180);
- @Documentation("The auto-generated node ID will be stored in this file and read after restarts. It is a good idea " +
- "to use an absolute file path here if you are starting Graylog DataNode from init scripts or similar.")
+ @Documentation("""
+ The auto-generated node ID will be stored in this file and read after restarts. It is a good idea
+ to use an absolute file path here if you are starting Graylog DataNode from init scripts or similar.
+ """)
@Parameter(value = "node_id_file", validators = NodeIdFileValidator.class)
private String nodeIdFile = "data/node-id";
@@ -195,15 +226,19 @@ public class Configuration {
@Parameter(value = "clustername")
private String clustername = "datanode-cluster";
- @Documentation("This configuration should be used if you want to connect to this Graylog DataNode's REST API and it is available on " +
- "another network interface than $http_bind_address, " +
- "for example if the machine has multiple network interfaces or is behind a NAT gateway.")
- @Parameter(value = "http_publish_uri", validators = URIAbsoluteValidator.class)
+ @Documentation("""
+ This configuration should be used if you want to connect to this Graylog DataNode's REST API
+ and it is available on another network interface than $http_bind_address,
+ for example if the machine has multiple network interfaces or is behind a NAT gateway.
+ """)
+ @Parameter(value = "http_publish_uri", validators = URIAbsoluteValidator.class)
private URI httpPublishUri;
- @Documentation("Enable GZIP support for HTTP interface. This compresses API responses and therefore helps to reduce " +
- " overall round trip times.")
+ @Documentation("""
+ Enable GZIP support for HTTP interface. This compresses API responses and therefore helps to reduce
+ overall round trip times.
+ """)
@Parameter(value = "http_enable_gzip")
private boolean httpEnableGzip = true;
@@ -286,7 +321,10 @@ public Integer getIndicesQueryBoolMaxClauseCount() {
return indicesQueryBoolMaxClauseCount;
}
- @Documentation("Configures verbosity of embedded opensearch logs. Possible values OFF, FATAL, ERROR, WARN, INFO, DEBUG, and TRACE, default is INFO")
+ @Documentation("""
+ Configures verbosity of embedded opensearch logs.
+ Possible values OFF, FATAL, ERROR, WARN, INFO, DEBUG, and TRACE, default is INFO
+ """)
@Parameter(value = "opensearch_logger_org_opensearch")
private String opensearchDebug;
diff --git a/data-node/src/main/java/org/graylog/datanode/docs/ConfigurationDocsGenerator.java b/data-node/src/main/java/org/graylog/datanode/docs/ConfigurationDocsGenerator.java
new file mode 100644
index 000000000000..f73d9199fe96
--- /dev/null
+++ b/data-node/src/main/java/org/graylog/datanode/docs/ConfigurationDocsGenerator.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2020 Graylog, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * .
+ */
+package org.graylog.datanode.docs;
+
+import com.github.joschi.jadconfig.Parameter;
+import com.github.joschi.jadconfig.ReflectionUtils;
+import jakarta.annotation.Nonnull;
+import org.apache.commons.lang3.ClassUtils;
+import org.graylog.datanode.commands.Server;
+import org.graylog.datanode.docs.printers.ConfigFileDocsPrinter;
+import org.graylog.datanode.docs.printers.ConfigurationSection;
+import org.graylog.datanode.docs.printers.CsvDocsPrinter;
+import org.graylog.datanode.docs.printers.DocsPrinter;
+import org.graylog2.configuration.Documentation;
+import org.graylog2.configuration.DocumentationSection;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class ConfigurationDocsGenerator {
+
+ /**
+ * This class is linked from the datanode pom.xml and generates conf.example and csv documentation.
+ */
+ public static void main(String[] args) throws IOException {
+ final ConfigurationDocsGenerator generator = new ConfigurationDocsGenerator();
+ generator.generateDocumentation(parseDocumentationFormat(args), ConfigurationDocsGenerator::getDatanodeConfigurationBeans);
+ }
+
+ @Nonnull
+ private static DocumentationFormat parseDocumentationFormat(String[] args) {
+ if (args.length != 2) {
+ throw new IllegalArgumentException("This command needs two arguments - a format and file path. For example" + "csv ${project.build.directory}/configuration-documentation.csv");
+ }
+ final String format = args[0].toLowerCase(Locale.ROOT).trim();
+ final String file = args[1];
+ return new DocumentationFormat(format, file);
+ }
+
+ protected void generateDocumentation(DocumentationFormat format, Supplier> configurationBeans) throws IOException {
+ final List sections = detectConfigurationSections(configurationBeans);
+ try (final DocsPrinter writer = createWriter(format)) {
+ writer.write(sections);
+ }
+ }
+
+ private DocsPrinter createWriter(DocumentationFormat format) throws IOException {
+ final FileWriter fileWriter = new FileWriter(format.outputFile(), StandardCharsets.UTF_8);
+ return switch (format.format()) {
+ case "csv" -> new CsvDocsPrinter(fileWriter);
+ case "conf" -> new ConfigFileDocsPrinter(fileWriter);
+ default -> throw new IllegalArgumentException("Unsupported format " + format.format());
+ };
+ }
+
+ /**
+ * Collects all configuration options from all available configuration beans.
+ */
+ private List detectConfigurationSections(Supplier> configurationBeans) {
+ return configurationBeans.get()
+ .stream()
+ .map(ConfigurationDocsGenerator::beanToConfigSections)
+ .sorted(Comparator.comparing(ConfigurationSection::hasPriority).reversed())
+ .toList();
+ }
+
+ @Nonnull
+ private static ConfigurationSection beanToConfigSections(Object configurationBean) {
+
+ String sectionHeading = null;
+ String sectionDescription = null;
+ if (configurationBean.getClass().isAnnotationPresent(DocumentationSection.class)) {
+ final DocumentationSection documentationSection = configurationBean.getClass().getAnnotation(DocumentationSection.class);
+ sectionHeading = documentationSection.heading();
+ sectionDescription = documentationSection.description();
+ }
+
+ final List entries = Arrays.stream(configurationBean.getClass().getDeclaredFields())
+ .filter(f -> f.isAnnotationPresent(Parameter.class))
+ .filter(ConfigurationDocsGenerator::isPublicFacing)
+ .map(f -> toConfigurationEntry(f, configurationBean))
+ .toList();
+
+ final List entriesWithoutSection = getEntriesWithoutSection(entries);
+ final List sortedSections = sectionsFromEntries(entries);
+
+ return new ConfigurationSection(sectionHeading, sectionDescription, sortedSections, entriesWithoutSection);
+ }
+
+ @Nonnull
+ private static List sectionsFromEntries(List entries) {
+ final Collection sections = entries.stream()
+ .filter(ConfigurationEntryWithSection::hasSection)
+ .collect(Collectors.groupingBy(ConfigurationEntryWithSection::sectionHeading, Collectors.collectingAndThen(Collectors.toList(), list -> new ConfigurationSection(list.iterator().next().sectionHeading(), list.iterator().next().sectionDescription(), Collections.emptyList(), list.stream().map(ConfigurationEntryWithSection::entry).collect(Collectors.toList()))))).values();
+ return sections.stream()
+ .sorted(Comparator.comparing(ConfigurationSection::hasPriority, Comparator.reverseOrder())).toList();
+ }
+
+ @Nonnull
+ private static List getEntriesWithoutSection(List entries) {
+ return entries.stream()
+ .filter(e -> !e.hasSection())
+ .map(ConfigurationEntryWithSection::entry)
+ .sorted(Comparator.comparing(ConfigurationEntry::hasPriority, Comparator.reverseOrder()))
+ .collect(Collectors.toList());
+ }
+
+ private static List