diff --git a/LICENSE b/LICENSE index 3a9303dddb6..0c1111bc79a 100644 --- a/LICENSE +++ b/LICENSE @@ -214,8 +214,3 @@ This product contains a modified part of Okio, distributed by Square: * License: Apache License v2.0 * Homepage: https://github.com/square/okio - -This product contains a modified part of Armeria, distributed by LINE Corporation: - - * License: Apache License v2.0 - * Homepage: https://github.com/line/armeria diff --git a/pom.xml b/pom.xml index 4692aa4abbf..6e906ece1d1 100755 --- a/pom.xml +++ b/pom.xml @@ -57,9 +57,9 @@ com.linecorp.armeria - 1.26.4 + 1.27.1 - 4.1.100.Final + 4.1.106.Final 2.16.1 @@ -564,8 +564,6 @@ build-bin/configure_test build-bin/deploy build-bin/test - - **/HttpFile.java true diff --git a/zipkin-server/README.md b/zipkin-server/README.md index c311c21dd61..981bf94893a 100644 --- a/zipkin-server/README.md +++ b/zipkin-server/README.md @@ -512,13 +512,13 @@ Zipkin can register itself in [Eureka](https://github.com/Netflix/eureka), allow to discover its listen address and health state. This is enabled when `EUREKA_SERVICE_URL` is set to a valid v2 endpoint of the [Eureka REST API](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations). -| Variable | Instance field | Description | -|----------------------------|----------------|------------------------------------------------------------------------------------------------| -| `DISCOVERY_EUREKA_ENABLED` | N/A | `false` disables Eureka registration. Defaults to `true`. | -| `EUREKA_SERVICE_URL` | N/A | v2 endpoint of Eureka, e.g. `https://eureka-prod/eureka/v2`. No default | -| `EUREKA_APP_NAME` | .app | The application this instance registers to. Defaults to `zipkin` | -| `EUREKA_HOSTNAME` | .hostName | The hostname used with `${QUERY_PORT}` to build the instance `vipAddress`. Defaults to detect. | -| `EUREKA_INSTANCE_ID` | .instanceId | Defaults to `${EUREKA_HOSTNAME}:${EUREKA_APP_NAME}:${QUERY_PORT}`. | +| Variable | Instance field | Description | +|----------------------------|----------------|-------------------------------------------------------------------------| +| `DISCOVERY_EUREKA_ENABLED` | N/A | `false` disables Eureka registration. Defaults to `true`. | +| `EUREKA_SERVICE_URL` | N/A | v2 endpoint of Eureka, e.g. `https://eureka-prod/eureka/v2`. No default | +| `EUREKA_APP_NAME` | .app | The application this instance registers to. Defaults to `zipkin` | +| `EUREKA_HOSTNAME` | .hostName | The instance `hostName` and `vipAddress`. Defaults to detect. | +| `EUREKA_INSTANCE_ID` | .instanceId | Defaults to `${EUREKA_HOSTNAME}:${EUREKA_APP_NAME}:${QUERY_PORT}`. | Example usage: diff --git a/zipkin-server/src/main/java/com/linecorp/armeria/server/file/HttpFile.java b/zipkin-server/src/main/java/com/linecorp/armeria/server/file/HttpFile.java deleted file mode 100644 index ec232e670ff..00000000000 --- a/zipkin-server/src/main/java/com/linecorp/armeria/server/file/HttpFile.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2019 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package com.linecorp.armeria.server.file; - -import static java.util.Objects.requireNonNull; - -import java.io.File; -import java.net.JarURLConnection; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Executor; - -import com.linecorp.armeria.common.HttpData; -import com.linecorp.armeria.common.HttpResponse; -import com.linecorp.armeria.common.ResponseHeaders; -import com.linecorp.armeria.common.annotation.Nullable; -import com.linecorp.armeria.common.annotation.UnstableApi; -import com.linecorp.armeria.server.HttpService; -import com.linecorp.armeria.server.file.HttpFileBuilder.ClassPathHttpFileBuilder; -import com.linecorp.armeria.server.file.HttpFileBuilder.FileSystemHttpFileBuilder; -import com.linecorp.armeria.server.file.HttpFileBuilder.HttpDataFileBuilder; -import com.linecorp.armeria.server.file.HttpFileBuilder.NonExistentHttpFileBuilder; -import com.linecorp.armeria.unsafe.PooledObjects; - -import io.netty.buffer.ByteBufAllocator; - -/** - * A file-like HTTP resource which yields an {@link HttpResponse}. - *
{@code
- * HttpFile faviconFile = HttpFile.of(new File("/var/www/favicon.ico"));
- * ServerBuilder builder = Server.builder();
- * builder.service("/favicon.ico", faviconFile.asService());
- * Server server = builder.build();
- * }
- * - * @see HttpFileBuilder - */ -public interface HttpFile { - - /** - * Creates a new {@link HttpFile} which streams the specified {@link File}. - */ - static HttpFile of(File file) { - return builder(file).build(); - } - - /** - * Creates a new {@link HttpFile} which streams the file at the specified {@link Path}. - */ - static HttpFile of(Path path) { - return builder(path).build(); - } - - /** - * Creates a new {@link HttpFile} which streams the resource at the specified {@code path}, loaded by - * the specified {@link ClassLoader}. - * - * @param classLoader the {@link ClassLoader} which will load the resource at the {@code path} - * @param path the path to the resource - */ - static HttpFile of(ClassLoader classLoader, String path) { - return builder(classLoader, path).build(); - } - - /** - * Creates a new {@link HttpFile} which streams the specified {@link HttpData}. This method is - * a shortcut for {@code HttpFile.of(data, System.currentTimeMillis()}. - */ - static HttpFile of(HttpData data) { - return builder(data).build(); - } - - /** - * Creates a new {@link HttpFile} which streams the specified {@link HttpData} with the specified - * {@code lastModifiedMillis}. - * - * @param data the data that provides the content of an HTTP response - * @param lastModifiedMillis when the {@code data} has been last modified, represented as the number of - * millisecond since the epoch - */ - static HttpFile of(HttpData data, long lastModifiedMillis) { - return builder(data, lastModifiedMillis).build(); - } - - /** - * Creates a new {@link HttpFile} which caches the content and attributes of the specified {@link HttpFile}. - * The cache is automatically invalidated when the {@link HttpFile} is updated. - * - * @param file the {@link HttpFile} to cache - * @param maxCachingLength the maximum allowed length of the {@link HttpFile} to cache. if the length of - * the {@link HttpFile} exceeds this value, no caching will be performed. - */ - static HttpFile ofCached(HttpFile file, int maxCachingLength) { - requireNonNull(file, "file"); - if (maxCachingLength<0){ - return null; - } - if (maxCachingLength == 0) { - return file; - } else { - return new CachingHttpFile(file, maxCachingLength); - } - } - - /** - * Returns an {@link HttpFile} which represents a non-existent file. - */ - static HttpFile nonExistent() { - return NonExistentHttpFile.INSTANCE; - } - - /** - * Returns an {@link HttpFile} redirected to the specified {@code location}. - */ - static HttpFile ofRedirect(String location) { - requireNonNull(location, "location"); - return new NonExistentHttpFile(location); - } - - /** - * Returns an {@link HttpFile} that becomes readable when the specified {@link CompletionStage} is complete. - * All {@link HttpFile} operations will wait until the specified {@link CompletionStage} is completed. - */ - static HttpFile from(CompletionStage stage) { - return new DeferredHttpFile(requireNonNull(stage, "stage")); - } - - /** - * Returns a new {@link HttpFileBuilder} that builds an {@link HttpFile} from the file at the specified - * {@link File}. - */ - static HttpFileBuilder builder(File file) { - return builder(requireNonNull(file, "file").toPath()); - } - - /** - * Returns a new {@link HttpFileBuilder} that builds an {@link HttpFile} from the file at the specified - * {@link Path}. - */ - static HttpFileBuilder builder(Path path) { - return new FileSystemHttpFileBuilder(requireNonNull(path, "path")); - } - - /** - * Returns a new {@link HttpFileBuilder} that builds an {@link HttpFile} from the specified - * {@link HttpData}. The last modified date of the file is set to 'now'. - */ - static HttpFileBuilder builder(HttpData data) { - return builder(data, System.currentTimeMillis()); - } - - /** - * Returns a new {@link HttpFileBuilder} that builds an {@link HttpFile} from the specified - * {@link HttpData} and {@code lastModifiedMillis}. - * - * @param data the content of the file - * @param lastModifiedMillis the last modified time represented as the number of milliseconds - * since the epoch - */ - static HttpFileBuilder builder(HttpData data, long lastModifiedMillis) { - requireNonNull(data, "data"); - return new HttpDataFileBuilder(data, lastModifiedMillis) - .autoDetectedContentType(false); // Can't auto-detect because there's no path or URI. - } - - /** - * Returns a new {@link HttpFileBuilder} that builds an {@link HttpFile} from the specified - * {@link URL}. {@code file:}, {@code jrt:} and {@linkplain JarURLConnection jar:file:} protocol. - */ - static HttpFileBuilder builder(URL url) { - requireNonNull(url, "url"); - if (url.getPath().endsWith("/")) { - // Non-existent resource. - return new NonExistentHttpFileBuilder(); - } - - // Convert to a real file if possible. - if ("file".equals(url.getProtocol())) { - File f; - try { - f = new File(url.toURI()); - } catch (URISyntaxException ignored) { - f = new File(url.getPath()); - } - - return builder(f.toPath()); - } else if ("jar".equals(url.getProtocol()) && (url.getPath().startsWith("file:")||url.getPath().startsWith("nested:")) || - "jrt".equals(url.getProtocol()) || - "bundle".equals(url.getProtocol())) { - return new ClassPathHttpFileBuilder(url); - } - throw new IllegalArgumentException("Unsupported URL: " + url + " (must start with " + - "'file:', 'jar:file', 'jrt:' or 'bundle:')"); - } - - /** - * Returns a new {@link HttpFileBuilder} that builds an {@link HttpFile} from the classpath resource - * at the specified {@code path} using the specified {@link ClassLoader}. - */ - static HttpFileBuilder builder(ClassLoader classLoader, String path) { - requireNonNull(classLoader, "classLoader"); - requireNonNull(path, "path"); - - // Strip the leading slash. - if (path.startsWith("/")) { - path = path.substring(1); - } - - // Retrieve the resource URL. - @Nullable - final URL url = classLoader.getResource(path); - if (url == null) { - // Non-existent resource. - return new NonExistentHttpFileBuilder(); - } - return builder(url); - } - - /** - * Retrieves the attributes of this file. - * - * @param fileReadExecutor the {@link Executor} which will perform the read operations against the file - * - * @return the {@link CompletableFuture} that will be completed with the attributes of this file. - * It will be completed with {@code null} if the file does not exist. - */ - CompletableFuture<@Nullable HttpFileAttributes> readAttributes(Executor fileReadExecutor); - - /** - * Reads the attributes of this file as {@link ResponseHeaders}, which could be useful for building - * a response for a {@code HEAD} request. - * - * @param fileReadExecutor the {@link Executor} which will perform the read operations against the file - * - * @return the {@link CompletableFuture} that will be completed with the headers. - * It will be completed with {@code null} if the file does not exist. - */ - CompletableFuture<@Nullable ResponseHeaders> readHeaders(Executor fileReadExecutor); - - /** - * Starts to stream this file into the returned {@link HttpResponse}. - * - * @param fileReadExecutor the {@link Executor} which will perform the read operations against the file - * @param alloc the {@link ByteBufAllocator} which will allocate the buffers that hold the content of - * the file - * @return the {@link CompletableFuture} that will be completed with the response. - * It will be completed with {@code null} if the file does not exist. - */ - CompletableFuture<@Nullable HttpResponse> read(Executor fileReadExecutor, ByteBufAllocator alloc); - - /** - * Converts this file into an {@link AggregatedHttpFile}. - * - * @param fileReadExecutor the {@link Executor} which will perform the read operations against the file - * - * @return a {@link CompletableFuture} which will complete when the aggregation process is finished, or - * a {@link CompletableFuture} successfully completed with {@code this}, if this file is already - * an {@link AggregatedHttpFile}. - */ - CompletableFuture aggregate(Executor fileReadExecutor); - - /** - * (Advanced users only) Converts this file into an {@link AggregatedHttpFile}. - * {@link AggregatedHttpFile#content()} will return a pooled {@link HttpData}, and the caller must - * ensure to release it. If you don't know what this means, use {@link #aggregate(Executor)}. - * - * @param fileReadExecutor the {@link Executor} which will perform the read operations against the file - * @param alloc the {@link ByteBufAllocator} which will allocate the content buffer - * - * @return a {@link CompletableFuture} which will complete when the aggregation process is finished, or - * a {@link CompletableFuture} successfully completed with {@code this}, if this file is already - * an {@link AggregatedHttpFile}. - * - * @see PooledObjects - */ - @UnstableApi - CompletableFuture aggregateWithPooledObjects(Executor fileReadExecutor, - ByteBufAllocator alloc); - - /** - * Returns an {@link HttpService} which serves the file for {@code HEAD} and {@code GET} requests. - */ - HttpService asService(); -} diff --git a/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryConfiguration.java b/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryConfiguration.java index 3582b6a641a..d3b44aef49a 100644 --- a/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryConfiguration.java +++ b/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryConfiguration.java @@ -13,6 +13,7 @@ */ package zipkin2.server.internal.eureka; +import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.eureka.EurekaUpdatingListener; import com.linecorp.armeria.spring.ArmeriaServerConfigurator; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; diff --git a/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryProperties.java b/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryProperties.java index 22344b66587..2d562e0e5b1 100644 --- a/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryProperties.java +++ b/zipkin-server/src/main/java/zipkin2/server/internal/eureka/ZipkinEurekaDiscoveryProperties.java @@ -14,12 +14,14 @@ package zipkin2.server.internal.eureka; import com.linecorp.armeria.common.auth.BasicToken; +import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.eureka.EurekaUpdatingListener; import com.linecorp.armeria.server.eureka.EurekaUpdatingListenerBuilder; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; import org.springframework.boot.context.properties.ConfigurationProperties; +import zipkin2.storage.cassandra.internal.HostAndPort; /** * Settings for Eureka registration. @@ -105,11 +107,17 @@ public void setHostname(String hostname) { } EurekaUpdatingListenerBuilder toBuilder() { - EurekaUpdatingListenerBuilder result = EurekaUpdatingListener.builder(serviceUrl); + EurekaUpdatingListenerBuilder result = EurekaUpdatingListener.builder(serviceUrl) + .healthCheckUrlPath("/health"); if (auth != null) result.auth(auth); if (appName != null) result.appName(appName); if (instanceId != null) result.instanceId(instanceId); - if (hostname != null) result.hostname(hostname); + if (hostname != null) { + result.hostname(hostname); + // Armeria defaults the vipAddress incorrectly to include the port. This is redundant with + // the server port, so override it until we have a PR to fix that. + result.vipAddress(hostname); + } return result; } diff --git a/zipkin-server/src/main/resources/zipkin-server-shared.yml b/zipkin-server/src/main/resources/zipkin-server-shared.yml index aa6e65e3e55..0ac443e1446 100644 --- a/zipkin-server/src/main/resources/zipkin-server-shared.yml +++ b/zipkin-server/src/main/resources/zipkin-server-shared.yml @@ -199,11 +199,6 @@ server: min-response-size: 2048 armeria: - # TODO: This is only to help with Eureka. After line/armeria#5369, add the - # same in EurekaUpdatingListenerBuilder#toBuilder and remove from here. This - # will remove the following log warning: - # WARN RejectedRouteHandler - Virtual host '*' has a duplicate route: /health - healthCheckPath: /health ports: - port: ${server.port} protocols: diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/eureka/BaseITZipkinEureka.java b/zipkin-server/src/test/java/zipkin2/server/internal/eureka/BaseITZipkinEureka.java index 5434787a56d..cc1a7c2882e 100644 --- a/zipkin-server/src/test/java/zipkin2/server/internal/eureka/BaseITZipkinEureka.java +++ b/zipkin-server/src/test/java/zipkin2/server/internal/eureka/BaseITZipkinEureka.java @@ -23,7 +23,6 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; @@ -60,7 +59,8 @@ "zipkin.storage.type=", // cheat and test empty storage type "zipkin.collector.http.enabled=false", "zipkin.query.enabled=false", - "zipkin.ui.enabled=false" + "zipkin.ui.enabled=false", + "zipkin.discovery.eureka.hostname=localhost" }) @Tag("docker") @Testcontainers(disabledWithoutDocker = true) @@ -96,17 +96,16 @@ abstract class BaseITZipkinEureka { assertThat(readString(json, "$.application.instance[0].status")) .isEqualTo("UP"); - String zipkinHostname = zipkin.defaultHostname(); int zipkinPort = zipkin.activePort().localAddress().getPort(); // Note: Netflix/Eureka says use hostname, which can conflict on laptops. // Armeria adopts the spring-cloud-netflix convention shown here. assertThat(readString(json, "$.application.instance[0].instanceId")) - .isEqualTo(zipkinHostname + ":zipkin:" + zipkinPort); + .isEqualTo("localhost:zipkin:" + zipkinPort); - // Make sure the vip address is relevant + // Make sure the vip address does not include the port! assertThat(readString(json, "$.application.instance[0].vipAddress")) - .isEqualTo(zipkinHostname + ":" + zipkinPort); + .isEqualTo("localhost"); } @Test @Order(2) void deregistersOnClose() { diff --git a/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/BulkCallBuilder.java b/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/BulkCallBuilder.java index 148c2019eba..b6e37996fa7 100644 --- a/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/BulkCallBuilder.java +++ b/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/BulkCallBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 The OpenZipkin Authors + * Copyright 2015-2024 The OpenZipkin Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -45,6 +45,7 @@ import static zipkin2.Call.propagateIfFatal; import static zipkin2.elasticsearch.ElasticsearchVersion.V7_0; import static zipkin2.elasticsearch.internal.JsonSerializers.OBJECT_MAPPER; +import static zipkin2.elasticsearch.internal.client.HttpCall.maybeRootCauseReason; // See https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html // exposed to re-use for testing writes of dependency links @@ -60,7 +61,7 @@ public final class BulkCallBuilder { // only throw when we know it is an error if (!root.at("/errors").booleanValue() && !root.at("/error").isObject()) return null; - String message = root.findPath("reason").textValue(); + String message = maybeRootCauseReason(root); if (message == null) message = contentString.get(); Number status = root.findPath("status").numberValue(); if (status != null && status.intValue() == 429) { diff --git a/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/HttpCall.java b/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/HttpCall.java index 6f9dd13d128..629a2763c11 100644 --- a/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/HttpCall.java +++ b/zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/HttpCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 The OpenZipkin Authors + * Copyright 2015-2024 The OpenZipkin Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -233,7 +233,7 @@ V parseResponse(AggregatedHttpResponse response, BodyConverter bodyConverter) String message = null; try { JsonNode root = OBJECT_MAPPER.readTree(parser); - message = root.findPath("reason").textValue(); + message = maybeRootCauseReason(root); if (message == null) message = root.at("/Message").textValue(); } catch (RuntimeException | IOException possiblyParseException) { // EmptyCatch ignored @@ -252,4 +252,15 @@ V parseResponse(AggregatedHttpResponse response, BodyConverter bodyConverter) return bodyConverter.convert(parser, content::toStringUtf8); } } + + public static String maybeRootCauseReason(JsonNode root) { + // Prefer the root cause to an arbitrary reason. + String message; + if (!root.findPath("root_cause").isMissingNode()) { + message = root.findPath("root_cause").findPath("reason").textValue(); + } else { + message = root.findPath("reason").textValue(); + } + return message; + } } diff --git a/zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/BulkCallBuilderTest.java b/zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/BulkCallBuilderTest.java index 6996cec416b..32c4a400ac0 100644 --- a/zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/BulkCallBuilderTest.java +++ b/zipkin-storage/elasticsearch/src/test/java/zipkin2/elasticsearch/internal/BulkCallBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 The OpenZipkin Authors + * Copyright 2015-2024 The OpenZipkin Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -32,13 +32,58 @@ class BulkCallBuilderTest { "rejected execution of org.elasticsearch.transport.TransportService$7@7ec1ea93 on EsThreadPoolExecutor[bulk, queue capacity = 200, org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor@621571ba[Running, pool size = 4, active threads = 4, queued tasks = 200, completed tasks = 3838534]]"); } - @Test void throwsRuntimeExceptionAsReasonWhenPresent() { - String response = - "{\"error\":{\"root_cause\":[{\"type\":\"illegal_argument_exception\",\"reason\":\"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\"}],\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"query\",\"grouped\":true,\"failed_shards\":[{\"shard\":0,\"index\":\"zipkin-2017-05-14\",\"node\":\"IqceAwZnSvyv0V0xALkEnQ\",\"reason\":{\"type\":\"illegal_argument_exception\",\"reason\":\"Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.\"}}]},\"status\":400}"; + @Test void throwsRuntimeExceptionAsRootCauseReasonWhenPresent() { + String response = """ + { + "error": { + "root_cause": [ + { + "type": "illegal_argument_exception", + "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead." + } + ], + "type": "search_phase_execution_exception", + "reason": "all shards failed", + "phase": "query", + "grouped": true, + "failed_shards": [ + { + "shard": 0, + "index": "zipkin-2017-05-14", + "node": "IqceAwZnSvyv0V0xALkEnQ", + "reason": { + "type": "illegal_argument_exception", + "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead." + } + } + ] + }, + "status": 400 + } + """; assertThatThrownBy( () -> CHECK_FOR_ERRORS.convert(JSON_FACTORY.createParser(response), () -> response)) .isInstanceOf(RuntimeException.class) .hasMessage("Fielddata is disabled on text fields by default. Set fielddata=true on [spanName] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."); } + + /** Tests lack of a root cause won't crash */ + @Test void throwsRuntimeExceptionAsReasonWhenPresent() { + String response = """ + { + "error": { + "type": "search_phase_execution_exception", + "reason": "all shards failed", + "phase": "query" + }, + "status": 400 + } + """; + + assertThatThrownBy( + () -> CHECK_FOR_ERRORS.convert(JSON_FACTORY.createParser(response), () -> response)) + .isInstanceOf(RuntimeException.class) + .hasMessage("all shards failed"); + } }