|
1 | 1 | package io.zentity.resolution; |
2 | 2 |
|
| 3 | +import com.fasterxml.jackson.core.JsonProcessingException; |
3 | 4 | import com.fasterxml.jackson.databind.JsonNode; |
4 | 5 | import com.fasterxml.jackson.databind.node.ArrayNode; |
5 | 6 | import com.fasterxml.jackson.databind.node.ObjectNode; |
|
14 | 15 | import io.zentity.resolution.input.Term; |
15 | 16 | import io.zentity.resolution.input.value.StringValue; |
16 | 17 | import io.zentity.resolution.input.value.Value; |
| 18 | +import org.elasticsearch.ElasticsearchException; |
17 | 19 | import org.elasticsearch.action.search.SearchAction; |
18 | 20 | import org.elasticsearch.action.search.SearchRequestBuilder; |
19 | 21 | import org.elasticsearch.action.search.SearchResponse; |
20 | 22 | import org.elasticsearch.client.node.NodeClient; |
| 23 | +import org.elasticsearch.common.Strings; |
21 | 24 | import org.elasticsearch.common.settings.Settings; |
22 | 25 | import org.elasticsearch.common.xcontent.DeprecationHandler; |
23 | 26 | import org.elasticsearch.common.xcontent.NamedXContentRegistry; |
| 27 | +import org.elasticsearch.common.xcontent.ToXContent; |
24 | 28 | import org.elasticsearch.common.xcontent.XContentFactory; |
25 | 29 | import org.elasticsearch.common.xcontent.XContentParser; |
26 | 30 | import org.elasticsearch.common.xcontent.XContentType; |
| 31 | +import org.elasticsearch.index.IndexNotFoundException; |
27 | 32 | import org.elasticsearch.search.SearchModule; |
28 | 33 | import org.elasticsearch.search.builder.SearchSourceBuilder; |
29 | 34 |
|
|
41 | 46 | import java.util.concurrent.atomic.AtomicInteger; |
42 | 47 | import java.util.regex.Pattern; |
43 | 48 |
|
| 49 | +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; |
44 | 50 | import static io.zentity.common.Patterns.COLON; |
45 | 51 |
|
46 | 52 | public class Job { |
@@ -72,15 +78,55 @@ public class Job { |
72 | 78 | private Map<String, Attribute> attributes = new TreeMap<>(); |
73 | 79 | private NodeClient client; |
74 | 80 | private Map<String, Set<String>> docIds = new TreeMap<>(); |
| 81 | + private String error = null; |
75 | 82 | private List<String> hits = new ArrayList<>(); |
76 | 83 | private int hop = 0; |
| 84 | + private Set<String> missingIndices = new TreeSet<>(); |
77 | 85 | private List<String> queries = new ArrayList<>(); |
78 | 86 | private boolean ran = false; |
79 | 87 |
|
80 | 88 | public Job(NodeClient client) { |
81 | 89 | this.client = client; |
82 | 90 | } |
83 | 91 |
|
| 92 | + public static String serializeElasticsearchException(ElasticsearchException e) throws IOException { |
| 93 | + String cause = Strings.toString(e.toXContent(jsonBuilder().startObject(), ToXContent.EMPTY_PARAMS).endObject()); |
| 94 | + return "{\"error\":{\"root_cause\":[" + cause + "],\"type\":\"" + ElasticsearchException.getExceptionName(e) + "\",\"reason\":\"" + e.getMessage() + "\"},\"status\":" + e.status().getStatus() + "}"; |
| 95 | + } |
| 96 | + |
| 97 | + public static String serializeLoggedQuery(Input input, int _hop, int _query, String indexName, String request, String response, List<String> resolvers, TreeMap<Integer, TreeMap<String, TreeMap>> resolversFilterTreeGrouped, List<String> termResolvers, TreeMap<String, TreeMap> termResolversFilterTree) throws JsonProcessingException { |
| 98 | + List<String> filtersLoggedList = new ArrayList<>(); |
| 99 | + if (!resolvers.isEmpty() && !resolversFilterTreeGrouped.isEmpty()) { |
| 100 | + List<String> attributesResolversSummary = new ArrayList<>(); |
| 101 | + for (String resolverName : resolvers) { |
| 102 | + List<String> resolversAttributes = new ArrayList<>(); |
| 103 | + for (String attributeName : input.model().resolvers().get(resolverName).attributes()) |
| 104 | + resolversAttributes.add("\"" + attributeName + "\""); |
| 105 | + attributesResolversSummary.add("\"" + resolverName + "\":{\"attributes\":[" + String.join(",", resolversAttributes) + "]}"); |
| 106 | + } |
| 107 | + String attributesResolversFilterTreeLogged = Json.ORDERED_MAPPER.writeValueAsString(resolversFilterTreeGrouped); |
| 108 | + filtersLoggedList.add("\"attributes\":{\"tree\":" + attributesResolversFilterTreeLogged + ",\"resolvers\":{" + String.join(",", attributesResolversSummary) + "}}"); |
| 109 | + } else { |
| 110 | + filtersLoggedList.add("\"attributes\":null"); |
| 111 | + } |
| 112 | + if (!termResolvers.isEmpty() && !termResolversFilterTree.isEmpty()) { |
| 113 | + List<String> termsResolversSummary = new ArrayList<>(); |
| 114 | + for (String resolverName : termResolvers) { |
| 115 | + List<String> resolverAttributes = new ArrayList<>(); |
| 116 | + for (String attributeName : input.model().resolvers().get(resolverName).attributes()) |
| 117 | + resolverAttributes.add("\"" + attributeName + "\""); |
| 118 | + termsResolversSummary.add("\"" + resolverName + "\":{\"attributes\":[" + String.join(",", resolverAttributes) + "]}"); |
| 119 | + } |
| 120 | + String termResolversFilterTreeLogged = Json.ORDERED_MAPPER.writeValueAsString(termResolversFilterTree); |
| 121 | + filtersLoggedList.add("\"terms\":{\"tree\":{\"0\":" + termResolversFilterTreeLogged + "},\"resolvers\":{" + String.join(",", termsResolversSummary) + "}}"); |
| 122 | + } else { |
| 123 | + filtersLoggedList.add("\"terms\":null"); |
| 124 | + } |
| 125 | + String filtersLogged = String.join(",", filtersLoggedList); |
| 126 | + String searchLogged = "{\"request\":" + request + ",\"response\":" + response + "}"; |
| 127 | + return "{\"_hop\":" + _hop + ",\"_query\":" + _query + ",\"_index\":\"" + indexName + "\",\"filters\":{" + filtersLogged + "},\"search\":" + searchLogged + "}"; |
| 128 | + } |
| 129 | + |
84 | 130 | public static String makeScriptFieldsClause(Input input, String indexName) throws ValidationException { |
85 | 131 | List<String> scriptFieldClauses = new ArrayList<>(); |
86 | 132 |
|
@@ -468,8 +514,10 @@ public static TreeMap<Integer, List<String>> groupResolversByWeight(Model model, |
468 | 514 | private void resetState() { |
469 | 515 | this.attributes = new TreeMap<>(this.input().attributes()); |
470 | 516 | this.docIds = new TreeMap<>(); |
| 517 | + this.error = null; |
471 | 518 | this.hits = new ArrayList<>(); |
472 | 519 | this.hop = 0; |
| 520 | + this.missingIndices = new TreeSet<>(); |
473 | 521 | this.queries = new ArrayList<>(); |
474 | 522 | this.ran = false; |
475 | 523 | } |
@@ -589,6 +637,10 @@ private void traverse() throws IOException, ValidationException { |
589 | 637 | // Construct a query for each index that maps to a resolver. |
590 | 638 | for (String indexName : this.input.model().indices().keySet()) { |
591 | 639 |
|
| 640 | + // Skip this index if a prior hop determined the index to be missing. |
| 641 | + if (this.missingIndices.contains(indexName)) |
| 642 | + continue; |
| 643 | + |
592 | 644 | // Track _ids for this index. |
593 | 645 | if (!this.docIds.containsKey(indexName)) |
594 | 646 | this.docIds.put(indexName, new TreeSet<>()); |
@@ -933,55 +985,55 @@ else if (!resolversClause.isEmpty()) |
933 | 985 | query = "{" + String.join(",", topLevelClauses) + "}"; |
934 | 986 |
|
935 | 987 | // Submit query to Elasticsearch. |
936 | | - SearchResponse response = this.search(indexName, query); |
| 988 | + SearchResponse response = null; |
| 989 | + ElasticsearchException responseError = null; |
| 990 | + boolean fatalError = false; |
| 991 | + try { |
| 992 | + response = this.search(indexName, query); |
| 993 | + } catch (IndexNotFoundException e) { |
| 994 | + // Don't fail the job if an index was missing. |
| 995 | + this.missingIndices.add(e.getIndex().getName()); |
| 996 | + responseError = e; |
| 997 | + } catch (ElasticsearchException e) { |
| 998 | + // Fail the job for any other error. |
| 999 | + fatalError = true; |
| 1000 | + responseError = e; |
| 1001 | + } |
937 | 1002 |
|
938 | 1003 | // Read response from Elasticsearch. |
939 | | - String responseBody = response.toString(); |
940 | | - JsonNode responseData = Json.ORDERED_MAPPER.readTree(responseBody); |
| 1004 | + JsonNode responseData = null; |
| 1005 | + if (response != null) |
| 1006 | + responseData = Json.ORDERED_MAPPER.readTree(response.toString()); |
941 | 1007 |
|
942 | 1008 | // Log queries. |
943 | 1009 | if (this.includeQueries || this.profile) { |
944 | | - JsonNode responseDataCopy = responseData.deepCopy(); |
945 | | - ObjectNode responseDataCopyObj = (ObjectNode) responseDataCopy; |
946 | | - if (responseDataCopyObj.has("hits")) { |
947 | | - ObjectNode responseDataCopyObjHits = (ObjectNode) responseDataCopyObj.get("hits"); |
948 | | - if (responseDataCopyObjHits.has("hits")) |
949 | | - responseDataCopyObjHits.remove("hits"); |
950 | | - } |
951 | | - List<String> filtersLoggedList = new ArrayList<>(); |
952 | | - if (!resolvers.isEmpty() && !resolversFilterTreeGrouped.isEmpty()) { |
953 | | - List<String> attributesResolversSummary = new ArrayList<>(); |
954 | | - for (String resolverName : resolvers) { |
955 | | - List<String> resolversAttributes = new ArrayList<>(); |
956 | | - for (String attributeName : input.model().resolvers().get(resolverName).attributes()) |
957 | | - resolversAttributes.add("\"" + attributeName + "\""); |
958 | | - attributesResolversSummary.add("\"" + resolverName + "\":{\"attributes\":[" + String.join(",", resolversAttributes) + "]}"); |
| 1010 | + String responseString; |
| 1011 | + if (responseData != null) { |
| 1012 | + JsonNode responseDataCopy = responseData.deepCopy(); |
| 1013 | + ObjectNode responseDataCopyObj = (ObjectNode) responseDataCopy; |
| 1014 | + if (responseDataCopyObj.has("hits")) { |
| 1015 | + ObjectNode responseDataCopyObjHits = (ObjectNode) responseDataCopyObj.get("hits"); |
| 1016 | + if (responseDataCopyObjHits.has("hits")) |
| 1017 | + responseDataCopyObjHits.remove("hits"); |
959 | 1018 | } |
960 | | - String attributesResolversFilterTreeLogged = Json.ORDERED_MAPPER.writeValueAsString(resolversFilterTreeGrouped); |
961 | | - filtersLoggedList.add("\"attributes\":{\"tree\":" + attributesResolversFilterTreeLogged + ",\"resolvers\":{" + String.join(",", attributesResolversSummary) + "}}"); |
| 1019 | + responseString = responseDataCopyObj.toString(); |
962 | 1020 | } else { |
963 | | - filtersLoggedList.add("\"attributes\":null"); |
| 1021 | + responseString = serializeElasticsearchException(responseError); |
964 | 1022 | } |
965 | | - if (!termResolvers.isEmpty() && !termResolversFilterTree.isEmpty()) { |
966 | | - List<String> termsResolversSummary = new ArrayList<>(); |
967 | | - for (String resolverName : termResolvers) { |
968 | | - List<String> resolverAttributes = new ArrayList<>(); |
969 | | - for (String attributeName : input.model().resolvers().get(resolverName).attributes()) |
970 | | - resolverAttributes.add("\"" + attributeName + "\""); |
971 | | - termsResolversSummary.add("\"" + resolverName + "\":{\"attributes\":[" + String.join(",", resolverAttributes) + "]}"); |
972 | | - } |
973 | | - String termResolversFilterTreeLogged = Json.ORDERED_MAPPER.writeValueAsString(termResolversFilterTree); |
974 | | - filtersLoggedList.add("\"terms\":{\"tree\":{\"0\":" + termResolversFilterTreeLogged + "},\"resolvers\":{" + String.join(",", termsResolversSummary) + "}}"); |
975 | | - } else { |
976 | | - filtersLoggedList.add("\"terms\":null"); |
977 | | - } |
978 | | - String filtersLogged = String.join(",", filtersLoggedList); |
979 | | - String searchLogged = "{\"request\":" + query + ",\"response\":" + responseDataCopyObj + "}"; |
980 | | - String logged = "{\"_hop\":" + this.hop + ",\"_query\":" + _query + ",\"_index\":\"" + indexName + "\",\"filters\":{" + filtersLogged + "},\"search\":" + searchLogged + "}"; |
| 1023 | + String logged = serializeLoggedQuery(this.input, this.hop, _query, indexName, query, responseString, resolvers, resolversFilterTreeGrouped, termResolvers, termResolversFilterTree); |
981 | 1024 | this.queries.add(logged); |
982 | 1025 | } |
983 | 1026 |
|
| 1027 | + // Stop traversing if there was an error not due to a missing index. |
| 1028 | + // Include the logged query in the response. |
| 1029 | + if (fatalError) { |
| 1030 | + this.error = serializeLoggedQuery(this.input, this.hop, _query, indexName, query, serializeElasticsearchException(responseError), resolvers, resolversFilterTreeGrouped, termResolvers, termResolversFilterTree); |
| 1031 | + return; |
| 1032 | + } |
| 1033 | + |
984 | 1034 | // Read the hits |
| 1035 | + if (responseData == null) |
| 1036 | + continue; |
985 | 1037 | if (!responseData.has("hits")) |
986 | 1038 | continue; |
987 | 1039 | if (!responseData.get("hits").has("hits")) |
@@ -1183,6 +1235,8 @@ public String run() throws IOException, ValidationException { |
1183 | 1235 | // Format response |
1184 | 1236 | List<String> responseParts = new ArrayList<>(); |
1185 | 1237 | responseParts.add("\"took\":" + Long.toString(took)); |
| 1238 | + if (this.error != null) |
| 1239 | + responseParts.add("\"error\":" + this.error); |
1186 | 1240 | if (this.includeHits) |
1187 | 1241 | responseParts.add("\"hits\":{\"total\":" + this.hits.size() + ",\"hits\":[" + String.join(",", this.hits) + "]}"); |
1188 | 1242 | if (this.includeQueries || this.profile) |
|
0 commit comments