Skip to content

Commit 9ce36cb

Browse files
Add http-event-listener.connect-http-headers.config-file property
Introduces the `http-event-listener.connect-http-headers.config-file` property to allow defining custom HTTP headers in an external file using `key=value` pairs. This supports header names and values containing commas (`,`) or colons (`:`), which are not supported in the inline `http-event-listener.connect-http-headers` property. Only one of `http-event-listener.connect-http-headers` or `http-event-listener.connect-http-headers.config-file` may be set. An exception will be raised if both are configured.
1 parent cea84a4 commit 9ce36cb

File tree

4 files changed

+190
-21
lines changed

4 files changed

+190
-21
lines changed

docs/src/main/sphinx/admin/event-listeners-http.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ event-listener.config-files=etc/http-event-listener.properties,...
7575
[](http-event-listener-custom-headers) for more details
7676
- Empty
7777

78+
* - http-event-listener.connect-http-headers.config-file
79+
- Path of the config file containing a list of custom HTTP headers to be sent along with the events. See
80+
[](http-event-listener-custom-headers) for more details
81+
- Empty
82+
7883
* - http-event-listener.connect-http-method
7984
- Specifies the HTTP method to use for the request. Supported values
8085
are POST and PUT.
@@ -119,8 +124,24 @@ Providing headers follows the pattern of `key:value` pairs separated by commas:
119124
http-event-listener.connect-http-headers="Header-Name-1:header value 1,Header-Value-2:header value 2,..."
120125
```
121126

122-
If you need to use a comma(`,`) or colon(`:`) in a header name or value,
123-
escape it using a backslash (`\`).
127+
If your header names or values need to include special characters such as commas
128+
(`,`) or colons (`:`),define them in an external configuration file using:
129+
130+
```text
131+
http-event-listener.connect-http-headers.config-file=/path/to/headers.conf
132+
```
133+
134+
The configuration file should contain one `key=value` pair per line, for example:
135+
136+
```text
137+
Header-Name-1=header value 1
138+
Header-Name-2=header value with : colon and , comma
139+
```
140+
141+
**Important:**
142+
Only one of `http-event-listener.connect-http-headers` **or**
143+
`http-event-listener.connect-http-headers.config-file` can be used at a time.
144+
If both properties are set, the system will raise an exception during startup.
124145

125-
Keep in mind that these are static, so they can not carry information
126-
taken from the event itself.
146+
Keep in mind that these headers are staticthey cannot include information
147+
dynamically taken from the event itself.

plugin/trino-http-event-listener/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@
157157
<scope>test</scope>
158158
</dependency>
159159

160+
<dependency>
161+
<groupId>io.airlift</groupId>
162+
<artifactId>testing</artifactId>
163+
<scope>test</scope>
164+
</dependency>
165+
160166
<dependency>
161167
<groupId>io.trino</groupId>
162168
<artifactId>trino-main</artifactId>

plugin/trino-http-event-listener/src/main/java/io/trino/plugin/httpquery/HttpEventListenerConfig.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,23 @@
1313
*/
1414
package io.trino.plugin.httpquery;
1515

16-
import com.google.common.collect.ImmutableMap;
1716
import io.airlift.configuration.Config;
1817
import io.airlift.configuration.ConfigDescription;
18+
import io.airlift.configuration.validation.FileExists;
1919
import io.airlift.units.Duration;
20+
import jakarta.validation.constraints.AssertTrue;
2021
import jakarta.validation.constraints.Min;
2122
import jakarta.validation.constraints.NotNull;
2223

24+
import java.io.File;
25+
import java.io.FileInputStream;
26+
import java.io.IOException;
27+
import java.io.UncheckedIOException;
2328
import java.util.EnumSet;
2429
import java.util.List;
2530
import java.util.Map;
31+
import java.util.Optional;
32+
import java.util.Properties;
2633
import java.util.stream.Collectors;
2734

2835
public class HttpEventListenerConfig
@@ -34,7 +41,22 @@ public class HttpEventListenerConfig
3441
private final EnumSet<HttpEventListenerEventType> loggedEvents = EnumSet.noneOf(HttpEventListenerEventType.class);
3542
private String ingestUri;
3643
private HttpEventListenerHttpMethod httpMethod = HttpEventListenerHttpMethod.POST;
37-
private Map<String, String> httpHeaders = ImmutableMap.of();
44+
private Map<String, String> httpHeaders = Map.of();
45+
private File httpHeadersConfigFile;
46+
47+
@Config("http-event-listener.connect-http-headers.config-file")
48+
@ConfigDescription("Path to a properties file containing custom HTTP headers, " +
49+
"specified as key-value pairs (one per line, e.g., Header-Name=Header Value)")
50+
public HttpEventListenerConfig setHttpHeadersConfigFile(File httpHeadersConfigFile)
51+
{
52+
this.httpHeadersConfigFile = httpHeadersConfigFile;
53+
return this;
54+
}
55+
56+
public Optional<@FileExists File> getHttpHeadersConfigFile()
57+
{
58+
return Optional.ofNullable(httpHeadersConfigFile);
59+
}
3860

3961
@ConfigDescription("Will log io.trino.spi.eventlistener.QueryCreatedEvent")
4062
@Config("http-event-listener.log-created")
@@ -111,6 +133,9 @@ public HttpEventListenerConfig setHttpMethod(HttpEventListenerHttpMethod httpMet
111133

112134
public Map<String, String> getHttpHeaders()
113135
{
136+
if (httpHeadersConfigFile != null) {
137+
return loadHttpHeadersFromFile(httpHeadersConfigFile);
138+
}
114139
return httpHeaders;
115140
}
116141

@@ -184,4 +209,26 @@ public Duration getMaxDelay()
184209
{
185210
return this.maxDelay;
186211
}
212+
213+
@AssertTrue(message = "Exactly one of http-event-listener.connect-http-headers.config-file or " +
214+
"http-event-listener.connect-http-headers must be set")
215+
public boolean validateHeaderConfigRedundant()
216+
{
217+
return !(httpHeadersConfigFile != null && !httpHeaders.isEmpty());
218+
}
219+
220+
private Map<String, String> loadHttpHeadersFromFile(File file)
221+
{
222+
Properties properties = new Properties();
223+
try (FileInputStream fis = new FileInputStream(file)) {
224+
properties.load(fis);
225+
}
226+
catch (IOException e) {
227+
throw new UncheckedIOException("Failed to read HTTP headers config file: " + file, e);
228+
}
229+
return properties.entrySet().stream()
230+
.collect(Collectors.toUnmodifiableMap(
231+
e -> e.getKey().toString(),
232+
e -> e.getValue().toString()));
233+
}
187234
}

plugin/trino-http-event-listener/src/test/java/io/trino/plugin/httpquery/TestHttpEventListenerConfig.java

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,76 @@
1313
*/
1414
package io.trino.plugin.httpquery;
1515

16+
import io.airlift.configuration.validation.FileExists;
1617
import io.airlift.units.Duration;
1718
import org.junit.jupiter.api.Test;
1819

20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
1924
import java.util.List;
2025
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.UUID;
2128
import java.util.concurrent.TimeUnit;
2229

2330
import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping;
2431
import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults;
2532
import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults;
33+
import static io.airlift.testing.ValidationAssertions.assertFailsValidation;
34+
import static org.assertj.core.api.Assertions.assertThat;
2635

2736
final class TestHttpEventListenerConfig
2837
{
2938
@Test
30-
void testDefaults()
39+
void testGetHttpHeadersLoadsFromConfigFile()
3140
throws Exception
41+
{
42+
// Create a temp file with header values in properties format, including single and double quotes
43+
Path tempFile = Files.createTempFile("headers", ".properties");
44+
String fileContent = "Authorization=Trust Me\nCache-Control=no-cache\nCustom-Header='single-quoted'\nAnother-Header=\"double-quoted\"\n";
45+
Files.writeString(tempFile, fileContent);
46+
47+
HttpEventListenerConfig config = new HttpEventListenerConfig();
48+
config.setHttpHeadersConfigFile(tempFile.toFile());
49+
50+
Map<String, String> headers = config.getHttpHeaders();
51+
assertThat(headers.size()).isEqualTo(4);
52+
assertThat(headers.get("Authorization")).isEqualTo("Trust Me");
53+
assertThat(headers.get("Cache-Control")).isEqualTo("no-cache");
54+
assertThat(headers.get("Custom-Header")).isEqualTo("'single-quoted'");
55+
assertThat(headers.get("Another-Header")).isEqualTo("\"double-quoted\"");
56+
}
57+
58+
@Test
59+
void testValidateHeaderConfigRedundant()
60+
throws IOException
61+
{
62+
HttpEventListenerConfig config = new HttpEventListenerConfig();
63+
// Neither set: valid
64+
assertThat(config.validateHeaderConfigRedundant()).isTrue();
65+
66+
// Only httpHeaders set: valid
67+
config.setHttpHeaders(List.of("Authorization: Trust Me"));
68+
assertThat(config.validateHeaderConfigRedundant()).isTrue();
69+
70+
// Only httpHeadersConfigFile set: valid
71+
config = new HttpEventListenerConfig();
72+
config.setHttpHeadersConfigFile(Files.createTempFile(null, null).toFile());
73+
assertThat(config.validateHeaderConfigRedundant()).isTrue();
74+
75+
// Both set: invalid
76+
config.setHttpHeaders(List.of("Authorization: Trust Me"));
77+
assertThat(config.validateHeaderConfigRedundant()).isFalse();
78+
}
79+
80+
@Test
81+
void testDefaults()
3282
{
3383
assertRecordedDefaults(recordDefaults(HttpEventListenerConfig.class)
3484
.setHttpHeaders(List.of())
85+
.setHttpHeadersConfigFile(null)
3586
.setIngestUri(null)
3687
.setRetryCount(0)
3788
.setRetryDelay(Duration.succinctDuration(1, TimeUnit.SECONDS))
@@ -44,33 +95,77 @@ void testDefaults()
4495
}
4596

4697
@Test
47-
void testExplicitPropertyMappings()
48-
throws Exception
98+
void testExplicitPropertyMappingsSkippingConnectHttpHeaders()
99+
throws IOException
49100
{
50-
Map<String, String> properties = Map.of(
51-
"http-event-listener.log-created", "true",
52-
"http-event-listener.log-completed", "true",
53-
"http-event-listener.log-split", "true",
54-
"http-event-listener.connect-ingest-uri", "http://example.com:8080/api",
55-
"http-event-listener.connect-http-headers", "Authorization: Trust Me, Cache-Control: no-cache",
56-
"http-event-listener.connect-retry-count", "2",
57-
"http-event-listener.connect-http-method", "PUT",
58-
"http-event-listener.connect-retry-delay", "101s",
59-
"http-event-listener.connect-backoff-base", "1.5",
60-
"http-event-listener.connect-max-delay", "10m");
101+
Path httpHeadersConfigFile = Files.createTempFile(null, null);
102+
103+
Map<String, String> properties = Map.ofEntries(
104+
Map.entry("http-event-listener.http-headers.config-file", httpHeadersConfigFile.toString()),
105+
Map.entry("http-event-listener.log-created", "true"),
106+
Map.entry("http-event-listener.log-completed", "true"),
107+
Map.entry("http-event-listener.log-split", "true"),
108+
Map.entry("http-event-listener.connect-ingest-uri", "http://example.com:8080/api"),
109+
Map.entry("http-event-listener.connect-retry-count", "2"),
110+
Map.entry("http-event-listener.connect-http-method", "PUT"),
111+
Map.entry("http-event-listener.connect-retry-delay", "101s"),
112+
Map.entry("http-event-listener.connect-backoff-base", "1.5"),
113+
Map.entry("http-event-listener.connect-max-delay", "10m"));
61114

62115
HttpEventListenerConfig expected = new HttpEventListenerConfig()
116+
.setHttpHeadersConfigFile(httpHeadersConfigFile.toFile())
63117
.setLogCompleted(true)
64118
.setLogCreated(true)
65119
.setLogSplit(true)
66120
.setIngestUri("http://example.com:8080/api")
121+
.setRetryCount(2)
122+
.setHttpMethod(HttpEventListenerHttpMethod.PUT)
123+
.setRetryDelay(Duration.succinctDuration(101, TimeUnit.SECONDS))
124+
.setBackoffBase(1.5)
125+
.setMaxDelay(Duration.succinctDuration(10, TimeUnit.MINUTES));
126+
127+
assertFullMapping(properties, expected, Set.of("http-event-listener.connect-http-headers"));
128+
}
129+
130+
@Test
131+
void testExplicitPropertyMappings()
132+
{
133+
Map<String, String> properties = Map.ofEntries(
134+
Map.entry("http-event-listener.connect-http-headers", "Authorization: Trust Me, Cache-Control: no-cache"),
135+
Map.entry("http-event-listener.log-created", "true"),
136+
Map.entry("http-event-listener.log-completed", "true"),
137+
Map.entry("http-event-listener.log-split", "true"),
138+
Map.entry("http-event-listener.connect-ingest-uri", "http://example.com:8080/api"),
139+
Map.entry("http-event-listener.connect-retry-count", "2"),
140+
Map.entry("http-event-listener.connect-http-method", "PUT"),
141+
Map.entry("http-event-listener.connect-retry-delay", "101s"),
142+
Map.entry("http-event-listener.connect-backoff-base", "1.5"),
143+
Map.entry("http-event-listener.connect-max-delay", "10m"));
144+
145+
HttpEventListenerConfig expected = new HttpEventListenerConfig()
67146
.setHttpHeaders(List.of("Authorization: Trust Me", "Cache-Control: no-cache"))
147+
.setLogCompleted(true)
148+
.setLogCreated(true)
149+
.setLogSplit(true)
150+
.setIngestUri("http://example.com:8080/api")
68151
.setRetryCount(2)
69152
.setHttpMethod(HttpEventListenerHttpMethod.PUT)
70153
.setRetryDelay(Duration.succinctDuration(101, TimeUnit.SECONDS))
71154
.setBackoffBase(1.5)
72155
.setMaxDelay(Duration.succinctDuration(10, TimeUnit.MINUTES));
73156

74-
assertFullMapping(properties, expected);
157+
assertFullMapping(properties, expected, Set.of("http-event-listener.http-headers.config-file"));
158+
}
159+
160+
@Test
161+
public void testConfigFileDoesNotExist()
162+
{
163+
File file = new File("/doesNotExist-" + UUID.randomUUID());
164+
assertFailsValidation(
165+
new HttpEventListenerConfig()
166+
.setHttpHeadersConfigFile(file),
167+
"httpHeadersConfigFile",
168+
"file does not exist: " + file,
169+
FileExists.class);
75170
}
76171
}

0 commit comments

Comments
 (0)