Skip to content

Commit 1a3eb07

Browse files
committed
WIP: Rewrite Env2yaml in java instead of Go
Managing a Go toolchain for persisting ENV vars in logstash container artifacts has become cumbersome. We already manage a java runtime so this commit presents a path forward to use that instead of Go. The Go binary is faster than java (in my testing Go would complete in around less than 200ms while java takes over 300ms). Given the container startup time is on the order of magnitute of seconds this change should be inperceptable to consumers. The benefit from consolidating in Java is worth the slightly lower performance.
1 parent 3ca7395 commit 1a3eb07

File tree

5 files changed

+213
-222
lines changed

5 files changed

+213
-222
lines changed

docker/Makefile

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ build-from-local-observability-sre-artifacts: dockerfile
6969

7070
COPY_FILES := $(ARTIFACTS_DIR)/docker/config/pipelines.yml $(ARTIFACTS_DIR)/docker/config/logstash-oss.yml $(ARTIFACTS_DIR)/docker/config/logstash-full.yml
7171
COPY_FILES += $(ARTIFACTS_DIR)/docker/config/log4j2.file.properties $(ARTIFACTS_DIR)/docker/config/log4j2.properties
72-
COPY_FILES += $(ARTIFACTS_DIR)/docker/env2yaml/env2yaml.go $(ARTIFACTS_DIR)/docker/env2yaml/go.mod $(ARTIFACTS_DIR)/docker/env2yaml/go.sum
72+
COPY_FILES += $(ARTIFACTS_DIR)/docker/env2yaml/Env2Yaml.java $(ARTIFACTS_DIR)/docker/env2yaml/env2yaml
7373
COPY_FILES += $(ARTIFACTS_DIR)/docker/pipeline/default.conf $(ARTIFACTS_DIR)/docker/bin/docker-entrypoint
7474

7575
$(ARTIFACTS_DIR)/docker/config/pipelines.yml: data/logstash/config/pipelines.yml
@@ -79,9 +79,8 @@ $(ARTIFACTS_DIR)/docker/config/log4j2.file.properties: data/logstash/config/log4
7979
$(ARTIFACTS_DIR)/docker/config/log4j2.properties: data/logstash/config/log4j2.properties
8080
$(ARTIFACTS_DIR)/docker/pipeline/default.conf: data/logstash/pipeline/default.conf
8181
$(ARTIFACTS_DIR)/docker/bin/docker-entrypoint: data/logstash/bin/docker-entrypoint
82-
$(ARTIFACTS_DIR)/docker/env2yaml/env2yaml.go: data/logstash/env2yaml/env2yaml.go
83-
$(ARTIFACTS_DIR)/docker/env2yaml/go.mod: data/logstash/env2yaml/go.mod
84-
$(ARTIFACTS_DIR)/docker/env2yaml/go.sum: data/logstash/env2yaml/go.sum
82+
$(ARTIFACTS_DIR)/docker/env2yaml/Env2Yaml.java: data/logstash/env2yaml/Env2Yaml.java
83+
$(ARTIFACTS_DIR)/docker/env2yaml/env2yaml: data/logstash/env2yaml/env2yaml
8584

8685
$(ARTIFACTS_DIR)/docker/%:
8786
cp -f $< $@
@@ -95,19 +94,17 @@ docker_paths:
9594

9695
COPY_IRONBANK_FILES := $(ARTIFACTS_DIR)/ironbank/scripts/config/pipelines.yml $(ARTIFACTS_DIR)/ironbank/scripts/config/logstash.yml
9796
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.file.properties $(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.properties
98-
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/pipeline/default.conf $(ARTIFACTS_DIR)/ironbank/scripts/bin/docker-entrypoint $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/env2yaml.go
99-
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.mod $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.sum $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/vendor/modules.txt $(ARTIFACTS_DIR)/ironbank/LICENSE $(ARTIFACTS_DIR)/ironbank/README.md
97+
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/pipeline/default.conf $(ARTIFACTS_DIR)/ironbank/scripts/bin/docker-entrypoint $(ARTIFACTS_DIR)/ironbank/scripts/java/Env2Yaml.java
98+
COPY_IRONBANK_FILES += $(ARTIFACTS_DIR)/ironbank/scripts/java/env2yaml $(ARTIFACTS_DIR)/ironbank/LICENSE $(ARTIFACTS_DIR)/ironbank/README.md
10099

101100
$(ARTIFACTS_DIR)/ironbank/scripts/config/pipelines.yml: data/logstash/config/pipelines.yml
102101
$(ARTIFACTS_DIR)/ironbank/scripts/config/logstash.yml: data/logstash/config/logstash-full.yml
103102
$(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.file.properties: data/logstash/config/log4j2.file.properties
104103
$(ARTIFACTS_DIR)/ironbank/scripts/config/log4j2.properties: data/logstash/config/log4j2.properties
105104
$(ARTIFACTS_DIR)/ironbank/scripts/pipeline/default.conf: data/logstash/pipeline/default.conf
106105
$(ARTIFACTS_DIR)/ironbank/scripts/bin/docker-entrypoint: data/logstash/bin/docker-entrypoint
107-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/env2yaml.go: data/logstash/env2yaml/env2yaml.go
108-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.mod: ironbank/go/src/env2yaml/go.mod
109-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/go.sum: ironbank/go/src/env2yaml/go.sum
110-
$(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/vendor/modules.txt: ironbank/go/src/env2yaml/vendor/modules.txt
106+
$(ARTIFACTS_DIR)/ironbank/scripts/java/Env2Yaml.java: data/logstash/env2yaml/Env2Yaml.java
107+
$(ARTIFACTS_DIR)/ironbank/scripts/java/env2yaml: data/logstash/env2yaml/env2yaml
111108
$(ARTIFACTS_DIR)/ironbank/LICENSE: ironbank/LICENSE
112109
$(ARTIFACTS_DIR)/ironbank/README.md: ironbank/README.md
113110

@@ -119,7 +116,7 @@ ironbank_docker_paths:
119116
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts
120117
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/bin
121118
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/config
122-
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/go/src/env2yaml/vendor
119+
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/java
123120
mkdir -p $(ARTIFACTS_DIR)/ironbank/scripts/pipeline
124121

125122
public-dockerfiles: public-dockerfiles_oss public-dockerfiles_full public-dockerfiles_wolfi public-dockerfiles_observability-sre public-dockerfiles_ironbank
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import org.yaml.snakeyaml.Yaml;
2+
import org.yaml.snakeyaml.DumperOptions;
3+
import java.io.*;
4+
import java.nio.charset.StandardCharsets;
5+
import java.nio.file.*;
6+
import java.nio.file.attribute.*;
7+
import java.time.LocalDateTime;
8+
import java.time.format.DateTimeFormatter;
9+
import java.util.*;
10+
11+
/**
12+
* Environment variable to YAML configuration merger
13+
*
14+
* Takes environment variables and merges them into logstash.yml
15+
* Example: docker run -e pipeline.workers=6
16+
* or: docker run -e PIPELINE_WORKERS=6
17+
* Result: pipeline.workers: 6 in logstash.yml
18+
*/
19+
public class Env2Yaml {
20+
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
21+
private static class SettingValidator {
22+
private final Map<String, String> normalizedToCanonical;
23+
24+
public SettingValidator() {
25+
this.normalizedToCanonical = buildSettingMap();
26+
}
27+
28+
private Map<String, String> buildSettingMap() {
29+
Map<String, String> map = new HashMap<>();
30+
String[] allowedConfigs = {
31+
"api.enabled", "api.http.host", "api.http.port", "api.environment",
32+
"node.name", "path.data", "pipeline.id", "pipeline.workers",
33+
"pipeline.output.workers", "pipeline.batch.size", "pipeline.batch.delay",
34+
"pipeline.unsafe_shutdown", "pipeline.ecs_compatibility", "pipeline.ordered",
35+
"pipeline.plugin_classloaders", "pipeline.separate_logs", "path.config",
36+
"config.string", "config.test_and_exit", "config.reload.automatic",
37+
"config.reload.interval", "config.debug", "config.support_escapes",
38+
"config.field_reference.escape_style", "queue.type", "path.queue",
39+
"queue.page_capacity", "queue.max_events", "queue.max_bytes",
40+
"queue.checkpoint.acks", "queue.checkpoint.writes", "queue.checkpoint.interval",
41+
"queue.compression", "queue.drain", "dead_letter_queue.enable",
42+
"dead_letter_queue.max_bytes", "dead_letter_queue.flush_interval",
43+
"dead_letter_queue.storage_policy", "dead_letter_queue.retain.age",
44+
"path.dead_letter_queue", "log.level", "log.format",
45+
"log.format.json.fix_duplicate_message_fields", "metric.collect",
46+
"path.logs", "path.plugins", "api.auth.type", "api.auth.basic.username",
47+
"api.auth.basic.password", "api.auth.basic.password_policy.mode",
48+
"api.auth.basic.password_policy.length.minimum", "api.auth.basic.password_policy.include.upper",
49+
"api.auth.basic.password_policy.include.lower", "api.auth.basic.password_policy.include.digit",
50+
"api.auth.basic.password_policy.include.symbol", "allow_superuser",
51+
"monitoring.cluster_uuid", "xpack.monitoring.allow_legacy_collection",
52+
"xpack.monitoring.enabled", "xpack.monitoring.collection.interval",
53+
"xpack.monitoring.elasticsearch.hosts", "xpack.monitoring.elasticsearch.username",
54+
"xpack.monitoring.elasticsearch.password", "xpack.monitoring.elasticsearch.proxy",
55+
"xpack.monitoring.elasticsearch.api_key", "xpack.monitoring.elasticsearch.cloud_auth",
56+
"xpack.monitoring.elasticsearch.cloud_id", "xpack.monitoring.elasticsearch.sniffing",
57+
"xpack.monitoring.elasticsearch.ssl.certificate_authority", "xpack.monitoring.elasticsearch.ssl.ca_trusted_fingerprint",
58+
"xpack.monitoring.elasticsearch.ssl.verification_mode", "xpack.monitoring.elasticsearch.ssl.truststore.path",
59+
"xpack.monitoring.elasticsearch.ssl.truststore.password", "xpack.monitoring.elasticsearch.ssl.keystore.path",
60+
"xpack.monitoring.elasticsearch.ssl.keystore.password", "xpack.monitoring.elasticsearch.ssl.certificate",
61+
"xpack.monitoring.elasticsearch.ssl.key", "xpack.monitoring.elasticsearch.ssl.cipher_suites",
62+
"xpack.management.enabled", "xpack.management.logstash.poll_interval",
63+
"xpack.management.pipeline.id", "xpack.management.elasticsearch.hosts",
64+
"xpack.management.elasticsearch.username", "xpack.management.elasticsearch.password",
65+
"xpack.management.elasticsearch.proxy", "xpack.management.elasticsearch.api_key",
66+
"xpack.management.elasticsearch.cloud_auth", "xpack.management.elasticsearch.cloud_id",
67+
"xpack.management.elasticsearch.sniffing", "xpack.management.elasticsearch.ssl.certificate_authority",
68+
"xpack.management.elasticsearch.ssl.ca_trusted_fingerprint", "xpack.management.elasticsearch.ssl.verification_mode",
69+
"xpack.management.elasticsearch.ssl.truststore.path", "xpack.management.elasticsearch.ssl.truststore.password",
70+
"xpack.management.elasticsearch.ssl.keystore.path", "xpack.management.elasticsearch.ssl.keystore.password",
71+
"xpack.management.elasticsearch.ssl.certificate", "xpack.management.elasticsearch.ssl.key",
72+
"xpack.management.elasticsearch.ssl.cipher_suites", "xpack.geoip.download.endpoint",
73+
"xpack.geoip.downloader.enabled"
74+
};
75+
76+
for (String configName : allowedConfigs) {
77+
String normalizedKey = normalizeKey(configName);
78+
map.put(normalizedKey, configName);
79+
}
80+
return map;
81+
}
82+
83+
public String findCanonicalSetting(String envVarName) {
84+
String normalized = normalizeKey(envVarName);
85+
return normalizedToCanonical.get(normalized);
86+
}
87+
}
88+
89+
private static String normalizeKey(String key) {
90+
return key.toLowerCase()
91+
.replace(".", "")
92+
.replace("_", "");
93+
}
94+
95+
public static void main(String[] args) {
96+
if (args.length != 1) {
97+
System.err.println("usage: env2yaml FILENAME");
98+
System.exit(1);
99+
}
100+
101+
try {
102+
new Env2Yaml().processConfigFile(args[0]);
103+
} catch (Exception e) {
104+
System.err.println("error: " + e.getMessage());
105+
System.exit(1);
106+
}
107+
}
108+
109+
private void processConfigFile(String configPath) throws Exception {
110+
Path fileLocation = Paths.get(configPath);
111+
Yaml yamlProcessor = new Yaml();
112+
113+
Map<String, Object> configData = loadExistingConfig(fileLocation, yamlProcessor);
114+
115+
SettingValidator validator = new SettingValidator();
116+
boolean addedNewConfigs = incorporateEnvironmentVars(configData, validator);
117+
118+
if (addedNewConfigs) {
119+
saveUpdatedConfig(fileLocation, yamlProcessor, configData);
120+
}
121+
}
122+
123+
@SuppressWarnings("unchecked")
124+
private Map<String, Object> loadExistingConfig(Path fileLocation, Yaml yamlProcessor) throws Exception {
125+
if (!Files.exists(fileLocation)) {
126+
return new HashMap<>();
127+
}
128+
129+
try (InputStream fileInput = Files.newInputStream(fileLocation)) {
130+
Object parsedData = yamlProcessor.load(fileInput);
131+
return parsedData instanceof Map ? (Map<String, Object>) parsedData : new HashMap<>();
132+
}
133+
}
134+
135+
private boolean incorporateEnvironmentVars(Map<String, Object> configData, SettingValidator validator) {
136+
boolean addedNewConfigs = false;
137+
138+
for (Map.Entry<String, String> envEntry : System.getenv().entrySet()) {
139+
String envVarName = envEntry.getKey();
140+
String envValue = envEntry.getValue();
141+
// Skip empty values like Go version does
142+
if (envValue == null || envValue.trim().isEmpty()) {
143+
continue;
144+
}
145+
String canonicalSetting = validator.findCanonicalSetting(envVarName);
146+
147+
if (canonicalSetting != null) {
148+
addedNewConfigs = true;
149+
System.err.println(LocalDateTime.now().format(TIMESTAMP_FORMAT) + " Setting '" + canonicalSetting + "' from environment.");
150+
configData.put(canonicalSetting, "${" + envVarName + "}");
151+
}
152+
}
153+
154+
return addedNewConfigs;
155+
}
156+
157+
private void saveUpdatedConfig(Path fileLocation, Yaml yamlProcessor, Map<String, Object> configData) throws Exception {
158+
// Configure YAML output to match Go version formatting (block style)
159+
DumperOptions options = new DumperOptions();
160+
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
161+
options.setPrettyFlow(true);
162+
options.setIndent(2);
163+
164+
Yaml blockYaml = new Yaml(options);
165+
String yamlOutput = blockYaml.dump(configData);
166+
167+
Set<PosixFilePermission> existingPermissions = getFilePermissions(fileLocation);
168+
169+
Files.write(fileLocation, yamlOutput.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
170+
171+
applyFilePermissions(fileLocation, existingPermissions);
172+
}
173+
174+
private Set<PosixFilePermission> getFilePermissions(Path fileLocation) {
175+
try {
176+
return Files.getPosixFilePermissions(fileLocation);
177+
} catch (UnsupportedOperationException e) {
178+
return null;
179+
} catch (Exception e) {
180+
return null;
181+
}
182+
}
183+
184+
private void applyFilePermissions(Path fileLocation, Set<PosixFilePermission> existingPermissions) {
185+
if (existingPermissions != null) {
186+
try {
187+
Files.setPosixFilePermissions(fileLocation, existingPermissions);
188+
} catch (Exception e) {
189+
// Ignore failures
190+
}
191+
}
192+
}
193+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
# Execute Java env2yaml using Logstash's bundled JDK and SnakeYAML
4+
# The .class file is compiled during Docker build and placed in /usr/local/bin
5+
6+
exec /usr/share/logstash/jdk/bin/java \
7+
-cp "/usr/share/logstash/logstash-core/lib/jars/*:/usr/local/bin" \
8+
Env2Yaml "$@"

0 commit comments

Comments
 (0)