Skip to content

Commit 9ab894c

Browse files
authored
feat: better expose HTTP Error header information in error handling on write (#745)
* feat: expose select HttpError headers in warning log. * chore: move logging code for Http Headers to more sensible WriteErrorEvent * docs: adds example of working with HTTP Errors on write. * chore: add license to new example * chore: remove unused imports and commented code * chore: remove commented line of code * chore: add WriteApi EventListener to example. * docs: update CHANGELOG.md * test: add handlesWriteApiHttpError test
1 parent 486dd13 commit 9ab894c

File tree

7 files changed

+228
-2
lines changed

7 files changed

+228
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Features
44

5+
- [#745](https://github.com/influxdata/influxdb-client-java/pull/745): New example `WriteHttpExceptionHandled.java` showing how to make use of `InfluxException.headers()` when HTTP Errors are returned from server. Also, now writes selected headers to client log.
56
- [#719](https://github.com/influxdata/influxdb-client-java/issues/719): `InfluxQLQueryService` header changes.
67
- `Accept` header can now be defined when making `InfluxQLQuery` calls. Supoorted MIME types:
78
- `application/csv`

client-core/src/test/java/com/influxdb/exceptions/InfluxExceptionTest.java

+26
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,32 @@ void messageContainsHttpErrorCode() {
322322
.matches((Predicate<Throwable>) throwable -> throwable.toString().equals("com.influxdb.exceptions.InfluxException: HTTP status code: 501; Message: Wrong query"));
323323
}
324324

325+
@Test
326+
void exceptionContainsHttpResponseHeaders() {
327+
Assertions.assertThatThrownBy(() -> {
328+
Response<Object> response = errorResponse(
329+
"not found",
330+
404,
331+
15,
332+
"not-json",
333+
"X-Platform-Error-Code",
334+
Map.of("Retry-After", "145",
335+
"Trace-ID", "1234567989ABCDEF0",
336+
"X-Influxdb-Build", "OSS"));
337+
throw new InfluxException(new HttpException(response));
338+
}
339+
).matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).status() == 404)
340+
.matches((Predicate<Throwable>) throwable -> throwable.getMessage().equals(
341+
"HTTP status code: 404; Message: not found"
342+
))
343+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().size() == 5)
344+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("Retry-After").equals("145"))
345+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("X-Influxdb-Build").equals("OSS"))
346+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("X-Influx-Reference").equals("15"))
347+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("X-Platform-Error-Code").equals("not found"))
348+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("Trace-ID").equals("1234567989ABCDEF0"));
349+
}
350+
325351
@Nonnull
326352
private Response<Object> errorResponse(@Nullable final String influxError) {
327353
return errorResponse(influxError, 500);

client/src/main/java/com/influxdb/client/write/events/WriteErrorEvent.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323

2424
import java.util.logging.Level;
2525
import java.util.logging.Logger;
26+
import java.util.stream.Stream;
2627
import javax.annotation.Nonnull;
2728

29+
import com.influxdb.exceptions.InfluxException;
2830
import com.influxdb.utils.Arguments;
2931

3032
/**
@@ -55,6 +57,21 @@ public Throwable getThrowable() {
5557

5658
@Override
5759
public void logEvent() {
58-
LOG.log(Level.SEVERE, "The error occurred during writing of data", throwable);
60+
if (throwable instanceof InfluxException ie) {
61+
String selectHeaders = Stream.of("trace-id",
62+
"trace-sampled",
63+
"X-Influxdb-Build",
64+
"X-Influxdb-Request-ID",
65+
"X-Influxdb-Version")
66+
.filter(name -> ie.headers().get(name) != null)
67+
.reduce("", (message, name) -> message.concat(String.format("%s: %s\n",
68+
name, ie.headers().get(name))));
69+
LOG.log(Level.SEVERE,
70+
String.format("An error occurred during writing of data. Select Response Headers:\n%s", selectHeaders),
71+
throwable);
72+
} else {
73+
LOG.log(Level.SEVERE, "An error occurred during writing of data", throwable);
74+
75+
}
5976
}
6077
}

client/src/test/java/com/influxdb/client/ITWriteApiBlocking.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
*/
2222
package com.influxdb.client;
2323

24+
import java.io.ByteArrayOutputStream;
25+
import java.io.PrintStream;
2426
import java.time.Instant;
2527
import java.util.Arrays;
2628
import java.util.List;
29+
import java.util.function.Predicate;
2730

2831
import com.influxdb.client.domain.WritePrecision;
2932
import com.influxdb.client.write.Point;
@@ -186,4 +189,22 @@ void defaultTags() {
186189
Assertions.assertThat(query.get(0).getRecords().get(0).getValueByKey("sensor-version")).isEqualTo("1.23a");
187190
Assertions.assertThat(query.get(0).getRecords().get(0).getValueByKey("env-var")).isEqualTo(System.getenv(envKey));
188191
}
189-
}
192+
193+
194+
@Test
195+
public void httpErrorHeaders(){
196+
Assertions.assertThatThrownBy(() -> {
197+
influxDBClient.getWriteApiBlocking().writeRecord(WritePrecision.MS, "asdf");
198+
}).isInstanceOf(InfluxException.class)
199+
.matches((Predicate<Throwable>) throwable -> throwable.getMessage().equals(
200+
"HTTP status code: 400; Message: unable to parse 'asdf': missing fields"
201+
))
202+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().keySet().size() == 6)
203+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("X-Influxdb-Build").equals("OSS"))
204+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("X-Influxdb-Version") != null)
205+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("X-Platform-Error-Code") != null)
206+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("Content-Length") != null)
207+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("Content-Type") != null)
208+
.matches((Predicate<Throwable>) throwable -> ((InfluxException) throwable).headers().get("Date") != null);
209+
}
210+
}

client/src/test/java/com/influxdb/client/ITWriteQueryApi.java

+32
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.concurrent.CountDownLatch;
31+
import java.util.concurrent.atomic.AtomicReference;
3132
import java.util.logging.Level;
3233
import java.util.logging.Logger;
3334

@@ -41,6 +42,7 @@
4142
import com.influxdb.client.write.Point;
4243
import com.influxdb.client.write.events.WriteErrorEvent;
4344
import com.influxdb.client.write.events.WriteSuccessEvent;
45+
import com.influxdb.exceptions.InfluxException;
4446
import com.influxdb.query.FluxRecord;
4547
import com.influxdb.query.FluxTable;
4648

@@ -860,4 +862,34 @@ public void queryParameters() {
860862
client.close();
861863
}
862864

865+
@Test
866+
public void handlesWriteApiHttpError(){
867+
868+
InfluxDBClient client = InfluxDBClientFactory.create(influxDB_URL, token.toCharArray());
869+
WriteApi writeApi = influxDBClient.makeWriteApi();
870+
AtomicReference<Boolean> called = new AtomicReference<>(false);
871+
872+
writeApi.listenEvents(WriteErrorEvent.class, (error) -> {
873+
called.set(true);
874+
Assertions.assertThat(error).isInstanceOf(WriteErrorEvent.class);
875+
Assertions.assertThat(error.getThrowable()).isInstanceOf(InfluxException.class);
876+
if(error.getThrowable() instanceof InfluxException ie){
877+
Assertions.assertThat(ie.headers()).isNotNull();
878+
Assertions.assertThat(ie.headers().keySet()).hasSize(6);
879+
Assertions.assertThat(ie.headers().get("Content-Length")).isNotNull();
880+
Assertions.assertThat(ie.headers().get("Content-Type")).contains("application/json");
881+
Assertions.assertThat(ie.headers().get("Date")).isNotNull();
882+
Assertions.assertThat(ie.headers().get("X-Influxdb-Build")).isEqualTo("OSS");
883+
Assertions.assertThat(ie.headers().get("X-Influxdb-Version")).startsWith("v");
884+
Assertions.assertThat(ie.headers().get("X-Platform-Error-Code")).isNotNull();
885+
}
886+
});
887+
888+
writeApi.writeRecord(bucket.getName(), organization.getId(), WritePrecision.MS, "asdf");
889+
writeApi.flush();
890+
writeApi.close();
891+
Assertions.assertThat(called.get()).as("WriteErrorEvent should have occurred")
892+
.isEqualTo(true);
893+
}
894+
863895
}

examples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This directory contains Java, Kotlin and Scala examples.
1818
- [InfluxDBEnterpriseExample.java](src/main/java/example/InfluxDBEnterpriseExample.java) - How to use `consistency` parameter for InfluxDB Enterprise
1919
- [RecordRowExample.java](src/main/java/example/RecordRowExample.java) - How to use `FluxRecord.getRow()` (List) instead of `FluxRecord.getValues()` (Map),
2020
in case of duplicity column names
21+
- [WriteHttpExceptionHandled](src/main/java/example/WriteHttpExceptionHandled.java) - How to work with HTTP Exceptions for debugging and recovery.
2122

2223
## Kotlin
2324

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*/
22+
package example;
23+
24+
import com.influxdb.client.InfluxDBClient;
25+
import com.influxdb.client.InfluxDBClientFactory;
26+
import com.influxdb.client.WriteApi;
27+
import com.influxdb.client.WriteApiBlocking;
28+
import com.influxdb.client.domain.WritePrecision;
29+
import com.influxdb.client.write.events.WriteErrorEvent;
30+
import com.influxdb.exceptions.InfluxException;
31+
32+
import javax.annotation.Nonnull;
33+
import java.time.Instant;
34+
import java.time.temporal.ChronoUnit;
35+
import java.util.List;
36+
import java.util.logging.Logger;
37+
38+
public class WriteHttpExceptionHandled {
39+
40+
static Logger Log = Logger.getLogger(WriteHttpExceptionHandled.class.getName());
41+
42+
public static String resolveProperty(final String property, final String fallback) {
43+
return System.getProperty(property, System.getenv(property)) == null
44+
? fallback : System.getProperty(property, System.getenv(property));
45+
}
46+
47+
private static final String influxUrl = resolveProperty("INFLUX_URL", "http://localhost:8086");
48+
private static final char[] token = resolveProperty("INFLUX_TOKEN","my-token").toCharArray();
49+
private static final String org = resolveProperty("INFLUX_ORG","my-org");
50+
private static final String bucket = resolveProperty("INFLUX_DATABASE","my-bucket");
51+
52+
public static void main(String[] args) {
53+
54+
InfluxDBClient influxDBClient = InfluxDBClientFactory.create(influxUrl, token, org, bucket);
55+
56+
WriteApiBlocking writeApiBlocking = influxDBClient.getWriteApiBlocking();
57+
WriteApi writeApi = influxDBClient.makeWriteApi();
58+
59+
// InfluxExceptions in Rx streams can be handled in an EventListener
60+
writeApi.listenEvents(WriteErrorEvent.class, (error) -> {
61+
if (error.getThrowable() instanceof InfluxException ie) {
62+
Log.warning("\n*** Custom event handler\n******\n"
63+
+ influxExceptionString(ie)
64+
+ "******\n");
65+
}
66+
});
67+
68+
// the following call will cause an HTTP 400 error
69+
writeApi.writeRecords(WritePrecision.MS, List.of("invalid", "clumsy", "broken", "unusable"));
70+
writeApi.close();
71+
72+
73+
Log.info("\nWriting invalid records to InfluxDB blocking - can handle caught InfluxException.\n");
74+
try {
75+
writeApiBlocking.writeRecord(WritePrecision.MS, "asdf");
76+
} catch (InfluxException e) {
77+
Log.info(influxExceptionString(e));
78+
}
79+
80+
// Note when writing batches with one bad record:
81+
// Cloud v3.x - The bad record is ignored.
82+
// OSS v2.x - returns exception
83+
Log.info("Writing Batch with 1 bad record.");
84+
Instant now = Instant.now();
85+
86+
List<String> lpData = List.of(
87+
String.format("temperature,location=north value=60.0 %d", now.toEpochMilli()),
88+
String.format("temperature,location=south value=65.0 %d", now.minus(1, ChronoUnit.SECONDS).toEpochMilli()),
89+
String.format("temperature,location=north value=59.8 %d", now.minus(2, ChronoUnit.SECONDS).toEpochMilli()),
90+
String.format("temperature,location=south value=64.8 %d", now.minus(3, ChronoUnit.SECONDS).toEpochMilli()),
91+
String.format("temperature,location=north value=59.7 %d", now.minus(4, ChronoUnit.SECONDS).toEpochMilli()),
92+
"asdf",
93+
String.format("temperature,location=north value=59.9 %d", now.minus(6, ChronoUnit.SECONDS).toEpochMilli()),
94+
String.format("temperature,location=south value=64.9 %d", now.minus(7, ChronoUnit.SECONDS).toEpochMilli()),
95+
String.format("temperature,location=north value=60.1 %d", now.minus(8, ChronoUnit.SECONDS).toEpochMilli()),
96+
String.format("temperature,location=south value=65.1 %d", now.minus(9, ChronoUnit.SECONDS).toEpochMilli())
97+
);
98+
99+
try {
100+
writeApiBlocking.writeRecords(WritePrecision.MS, lpData);
101+
} catch (InfluxException e) {
102+
Log.info(influxExceptionString(e));
103+
}
104+
105+
try {
106+
writeApi.writeRecords(WritePrecision.MS, lpData);
107+
} catch (Exception exception) {
108+
if (exception instanceof InfluxException) {
109+
Log.info(influxExceptionString((InfluxException) exception));
110+
}
111+
}
112+
Log.info("Done");
113+
}
114+
115+
private static String influxExceptionString(@Nonnull InfluxException e) {
116+
StringBuilder sBuilder = new StringBuilder().append("Handling InfluxException:\n");
117+
sBuilder.append(" ").append(e.getMessage());
118+
String headers = e.headers()
119+
.keySet()
120+
.stream()
121+
.reduce("\n", (set, key) -> set.concat(
122+
String.format(" %s: %s\n", key, e.headers().get(key)))
123+
);
124+
sBuilder.append("\n HTTP Response Headers:");
125+
sBuilder.append(headers);
126+
return sBuilder.toString();
127+
}
128+
}

0 commit comments

Comments
 (0)