Skip to content

Commit 592debd

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 592debd

File tree

5 files changed

+214
-26
lines changed

5 files changed

+214
-26
lines changed

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

Lines changed: 24 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,23 @@ 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+
Only one of `http-event-listener.connect-http-headers` **or**
142+
`http-event-listener.connect-http-headers.config-file` can be used at a time.
143+
If both properties are set, the system will raise an exception during startup.
124144

125-
Keep in mind that these are static, so they can not carry information
126-
taken from the event itself.
145+
Keep in mind that these headers are staticthey cannot include information
146+
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/HttpEventListener.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import com.google.common.util.concurrent.Futures;
2020
import com.google.inject.Inject;
2121
import io.airlift.bootstrap.LifeCycleManager;
22+
import io.airlift.configuration.validation.FileExists;
2223
import io.airlift.http.client.BodyGenerator;
2324
import io.airlift.http.client.HttpClient;
2425
import io.airlift.http.client.Request;
26+
import io.airlift.http.client.StatusResponseHandler.StatusResponse;
2527
import io.airlift.json.JsonCodec;
2628
import io.airlift.log.Logger;
2729
import io.airlift.units.Duration;
@@ -31,9 +33,11 @@
3133
import io.trino.spi.eventlistener.SplitCompletedEvent;
3234
import jakarta.annotation.PreDestroy;
3335

36+
import java.io.File;
3437
import java.net.URI;
3538
import java.net.URISyntaxException;
3639
import java.util.Map;
40+
import java.util.Optional;
3741
import java.util.concurrent.ScheduledExecutorService;
3842
import java.util.concurrent.TimeUnit;
3943

@@ -42,7 +46,6 @@
4246
import static com.google.common.net.MediaType.JSON_UTF_8;
4347
import static io.airlift.concurrent.Threads.daemonThreadsNamed;
4448
import static io.airlift.http.client.JsonBodyGenerator.jsonBodyGenerator;
45-
import static io.airlift.http.client.StatusResponseHandler.StatusResponse;
4649
import static io.airlift.http.client.StatusResponseHandler.createStatusResponseHandler;
4750
import static java.util.Objects.requireNonNull;
4851
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
@@ -73,6 +76,7 @@ public class HttpEventListener
7376
private final Duration maxDelay;
7477
private final double backoffBase;
7578
private final Map<String, String> httpHeaders;
79+
private final Optional<@FileExists File> httpHeadersConfigFile;
7680
private final URI ingestUri;
7781
private final HttpEventListenerHttpMethod httpMethod;
7882
private final ScheduledExecutorService executor;
@@ -102,6 +106,7 @@ public HttpEventListener(
102106
this.backoffBase = config.getBackoffBase();
103107
this.httpMethod = config.getHttpMethod();
104108
this.httpHeaders = ImmutableMap.copyOf(config.getHttpHeaders());
109+
this.httpHeadersConfigFile = config.getHttpHeadersConfigFile();
105110

106111
try {
107112
ingestUri = new URI(config.getIngestUri());
@@ -145,13 +150,22 @@ public void splitCompleted(SplitCompletedEvent splitCompletedEvent)
145150

146151
private void sendLog(BodyGenerator eventBodyGenerator, String queryId)
147152
{
148-
Request request = Request.builder()
153+
Request.Builder requestBuilder = Request.builder()
149154
.setMethod(httpMethod.name())
150-
.addHeaders(Multimaps.forMap(httpHeaders))
151155
.addHeader(CONTENT_TYPE, JSON_UTF_8.toString())
152156
.setUri(ingestUri)
153-
.setBodyGenerator(eventBodyGenerator)
154-
.build();
157+
.setBodyGenerator(eventBodyGenerator);
158+
159+
httpHeadersConfigFile.ifPresentOrElse(
160+
file -> {
161+
Map<String, String> fileHeaders = HttpEventListenerConfig.loadHttpHeadersFromFile(file);
162+
requestBuilder.addHeaders(Multimaps.forMap(fileHeaders));
163+
},
164+
() -> {
165+
requestBuilder.addHeaders(Multimaps.forMap(httpHeaders));
166+
});
167+
168+
Request request = requestBuilder.build();
155169

156170
attemptToSend(request, 0, Duration.valueOf("0s"), queryId);
157171
}

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,18 +13,27 @@
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

35+
import static com.google.common.collect.ImmutableMap.toImmutableMap;
36+
2837
public class HttpEventListenerConfig
2938
{
3039
private int retryCount;
@@ -34,7 +43,8 @@ public class HttpEventListenerConfig
3443
private final EnumSet<HttpEventListenerEventType> loggedEvents = EnumSet.noneOf(HttpEventListenerEventType.class);
3544
private String ingestUri;
3645
private HttpEventListenerHttpMethod httpMethod = HttpEventListenerHttpMethod.POST;
37-
private Map<String, String> httpHeaders = ImmutableMap.of();
46+
private Map<String, String> httpHeaders = Map.of();
47+
private File httpHeadersConfigFile;
3848

3949
@ConfigDescription("Will log io.trino.spi.eventlistener.QueryCreatedEvent")
4050
@Config("http-event-listener.log-created")
@@ -130,6 +140,20 @@ public HttpEventListenerConfig setHttpHeaders(List<String> httpHeaders)
130140
return this;
131141
}
132142

143+
public Optional<@FileExists File> getHttpHeadersConfigFile()
144+
{
145+
return Optional.ofNullable(httpHeadersConfigFile);
146+
}
147+
148+
@Config("http-event-listener.connect-http-headers.config-file")
149+
@ConfigDescription("Path to a properties file containing custom HTTP headers, " +
150+
"specified as key-value pairs (one per line, e.g., Header-Name=Header Value)")
151+
public HttpEventListenerConfig setHttpHeadersConfigFile(File httpHeadersConfigFile)
152+
{
153+
this.httpHeadersConfigFile = httpHeadersConfigFile;
154+
return this;
155+
}
156+
133157
@ConfigDescription("Number of retries on server error")
134158
@Config("http-event-listener.connect-retry-count")
135159
public HttpEventListenerConfig setRetryCount(int retryCount)
@@ -184,4 +208,27 @@ public Duration getMaxDelay()
184208
{
185209
return this.maxDelay;
186210
}
211+
212+
@AssertTrue(message = "Exactly one of http-event-listener.connect-http-headers.config-file or " +
213+
"http-event-listener.connect-http-headers must be set")
214+
public boolean validateHeaderConfigRedundant()
215+
{
216+
return !(httpHeadersConfigFile != null && !httpHeaders.isEmpty());
217+
}
218+
219+
public static Map<String, String> loadHttpHeadersFromFile(File file)
220+
{
221+
Properties properties = new Properties();
222+
try (FileInputStream fis = new FileInputStream(file)) {
223+
properties.load(fis);
224+
}
225+
catch (IOException e) {
226+
throw new UncheckedIOException("Failed to read HTTP headers config file: " + file, e);
227+
}
228+
229+
return properties.entrySet().stream()
230+
.collect(toImmutableMap(
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: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,81 @@
1313
*/
1414
package io.trino.plugin.httpquery;
1515

16+
import com.google.common.collect.ImmutableMap;
17+
import io.airlift.configuration.validation.FileExists;
1618
import io.airlift.units.Duration;
1719
import org.junit.jupiter.api.Test;
1820

21+
import java.io.File;
22+
import java.io.IOException;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
1925
import java.util.List;
2026
import java.util.Map;
27+
import java.util.Set;
28+
import java.util.UUID;
2129
import java.util.concurrent.TimeUnit;
2230

2331
import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping;
2432
import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults;
2533
import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults;
34+
import static io.airlift.testing.ValidationAssertions.assertFailsValidation;
35+
import static org.assertj.core.api.Assertions.assertThat;
2636

2737
final class TestHttpEventListenerConfig
2838
{
2939
@Test
30-
void testDefaults()
40+
void testGetHttpHeadersLoadsFromConfigFile()
3141
throws Exception
42+
{
43+
// Create a temp file with header values in properties format, including single and double quotes
44+
Path tempFile = Files.createTempFile("headers", ".properties");
45+
String fileContent = "Authorization=Trust Me\nCache-Control=no-cache\nCustom-Header='single-quoted'\nAnother-Header=\"double-quoted\"\n";
46+
Files.writeString(tempFile, fileContent);
47+
48+
HttpEventListenerConfig config = new HttpEventListenerConfig();
49+
config.setHttpHeadersConfigFile(tempFile.toFile());
50+
51+
Map<String, String> headers = config.getHttpHeadersConfigFile()
52+
.map(HttpEventListenerConfig::loadHttpHeadersFromFile)
53+
.orElseGet(Map::of);
54+
55+
assertThat(headers)
56+
.hasSize(4)
57+
.containsEntry("Authorization", "Trust Me")
58+
.containsEntry("Cache-Control", "no-cache")
59+
.containsEntry("Custom-Header", "'single-quoted'")
60+
.containsEntry("Another-Header", "\"double-quoted\"");
61+
}
62+
63+
@Test
64+
void testValidateHeaderConfigRedundant()
65+
throws IOException
66+
{
67+
HttpEventListenerConfig config = new HttpEventListenerConfig();
68+
// Neither set: valid
69+
assertThat(config.validateHeaderConfigRedundant()).isTrue();
70+
71+
// Only httpHeaders set: valid
72+
config.setHttpHeaders(List.of("Authorization: Trust Me"));
73+
assertThat(config.validateHeaderConfigRedundant()).isTrue();
74+
75+
// Only httpHeadersConfigFile set: valid
76+
config = new HttpEventListenerConfig();
77+
config.setHttpHeadersConfigFile(Files.createTempFile(null, null).toFile());
78+
assertThat(config.validateHeaderConfigRedundant()).isTrue();
79+
80+
// Both set: invalid
81+
config.setHttpHeaders(List.of("Authorization: Trust Me"));
82+
assertThat(config.validateHeaderConfigRedundant()).isFalse();
83+
}
84+
85+
@Test
86+
void testDefaults()
3287
{
3388
assertRecordedDefaults(recordDefaults(HttpEventListenerConfig.class)
3489
.setHttpHeaders(List.of())
90+
.setHttpHeadersConfigFile(null)
3591
.setIngestUri(null)
3692
.setRetryCount(0)
3793
.setRetryDelay(Duration.succinctDuration(1, TimeUnit.SECONDS))
@@ -44,33 +100,78 @@ void testDefaults()
44100
}
45101

46102
@Test
47-
void testExplicitPropertyMappings()
48-
throws Exception
103+
void testExplicitPropertyMappingsSkippingConnectHttpHeaders()
104+
throws IOException
49105
{
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");
106+
Path httpHeadersConfigFile = Files.createTempFile(null, null);
107+
108+
Map<String, String> properties = Map.ofEntries(
109+
Map.entry("http-event-listener.connect-http-headers.config-file", httpHeadersConfigFile.toString()),
110+
Map.entry("http-event-listener.log-created", "true"),
111+
Map.entry("http-event-listener.log-completed", "true"),
112+
Map.entry("http-event-listener.log-split", "true"),
113+
Map.entry("http-event-listener.connect-ingest-uri", "http://example.com:8080/api"),
114+
Map.entry("http-event-listener.connect-retry-count", "2"),
115+
Map.entry("http-event-listener.connect-http-method", "PUT"),
116+
Map.entry("http-event-listener.connect-retry-delay", "101s"),
117+
Map.entry("http-event-listener.connect-backoff-base", "1.5"),
118+
Map.entry("http-event-listener.connect-max-delay", "10m"));
61119

62120
HttpEventListenerConfig expected = new HttpEventListenerConfig()
121+
.setHttpHeadersConfigFile(httpHeadersConfigFile.toFile())
63122
.setLogCompleted(true)
64123
.setLogCreated(true)
65124
.setLogSplit(true)
66125
.setIngestUri("http://example.com:8080/api")
126+
.setRetryCount(2)
127+
.setHttpMethod(HttpEventListenerHttpMethod.PUT)
128+
.setRetryDelay(Duration.succinctDuration(101, TimeUnit.SECONDS))
129+
.setBackoffBase(1.5)
130+
.setMaxDelay(Duration.succinctDuration(10, TimeUnit.MINUTES));
131+
132+
assertFullMapping(properties, expected, Set.of("http-event-listener.connect-http-headers"));
133+
}
134+
135+
@Test
136+
void testExplicitPropertyMappings()
137+
{
138+
ImmutableMap<String, String> properties = ImmutableMap.<String, String>builder()
139+
.put("http-event-listener.connect-http-headers", "Authorization: Trust Me, Cache-Control: no-cache")
140+
.put("http-event-listener.log-created", "true")
141+
.put("http-event-listener.log-completed", "true")
142+
.put("http-event-listener.log-split", "true")
143+
.put("http-event-listener.connect-ingest-uri", "http://example.com:8080/api")
144+
.put("http-event-listener.connect-retry-count", "2")
145+
.put("http-event-listener.connect-http-method", "PUT")
146+
.put("http-event-listener.connect-retry-delay", "101s")
147+
.put("http-event-listener.connect-backoff-base", "1.5")
148+
.put("http-event-listener.connect-max-delay", "10m")
149+
.buildOrThrow();
150+
151+
HttpEventListenerConfig expected = new HttpEventListenerConfig()
67152
.setHttpHeaders(List.of("Authorization: Trust Me", "Cache-Control: no-cache"))
153+
.setLogCompleted(true)
154+
.setLogCreated(true)
155+
.setLogSplit(true)
156+
.setIngestUri("http://example.com:8080/api")
68157
.setRetryCount(2)
69158
.setHttpMethod(HttpEventListenerHttpMethod.PUT)
70159
.setRetryDelay(Duration.succinctDuration(101, TimeUnit.SECONDS))
71160
.setBackoffBase(1.5)
72161
.setMaxDelay(Duration.succinctDuration(10, TimeUnit.MINUTES));
73162

74-
assertFullMapping(properties, expected);
163+
assertFullMapping(properties, expected, Set.of("http-event-listener.connect-http-headers.config-file"));
164+
}
165+
166+
@Test
167+
void testConfigFileDoesNotExist()
168+
{
169+
File file = new File("/doesNotExist-" + UUID.randomUUID());
170+
assertFailsValidation(
171+
new HttpEventListenerConfig()
172+
.setHttpHeadersConfigFile(file),
173+
"httpHeadersConfigFile",
174+
"file does not exist: " + file,
175+
FileExists.class);
75176
}
76177
}

0 commit comments

Comments
 (0)