Skip to content

Commit 3d0a4e5

Browse files
authored
Initial implementation of report-builder interface (#5266)
1 parent 7e67fcb commit 3d0a4e5

34 files changed

Lines changed: 1355 additions & 177 deletions

File tree

common/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ repositories {
3232
mavenCentral()
3333
}
3434

35+
test {
36+
maxParallelForks = Runtime.runtime.availableProcessors()
37+
useJUnitPlatform()
38+
testLogging {
39+
events "passed", "skipped", "failed"
40+
exceptionFormat "full"
41+
}
42+
}
43+
3544
dependencies {
3645
api "org.antlr:antlr4-runtime:4.13.2"
3746
api group: 'com.google.guava', name: 'guava', version: "${guava_version}"
@@ -52,6 +61,8 @@ dependencies {
5261
testImplementation group: 'org.mockito', name: 'mockito-core', version: "${mockito_version}"
5362
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: "${mockito_version}"
5463
testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '4.12.0'
64+
65+
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
5566
}
5667

5768

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.common.error;
7+
8+
/**
9+
* Machine-readable error codes for categorizing exceptions. These codes help clients handle
10+
* specific error types programmatically. <br>
11+
* <br>
12+
* Not a complete list, currently seeded with some initial values. Feel free to add variants or
13+
* remove dead variants over time.
14+
*/
15+
public enum ErrorCode {
16+
/** Field not found in the index mapping */
17+
FIELD_NOT_FOUND,
18+
19+
/** Syntax error in query parsing */
20+
SYNTAX_ERROR,
21+
22+
/** Ambiguous field reference (multiple fields with same name) */
23+
AMBIGUOUS_FIELD,
24+
25+
/** Generic semantic validation error */
26+
SEMANTIC_ERROR,
27+
28+
/** Expression evaluation failed */
29+
EVALUATION_ERROR,
30+
31+
/** Type mismatch or type validation error */
32+
TYPE_ERROR,
33+
34+
/** Unsupported feature or operation */
35+
UNSUPPORTED_OPERATION,
36+
37+
/** Resource limit exceeded (memory, CPU, etc.) */
38+
RESOURCE_LIMIT_EXCEEDED,
39+
40+
/** Index or datasource not found */
41+
INDEX_NOT_FOUND,
42+
43+
/** Permission denied or insufficient privileges */
44+
PERMISSION_DENIED,
45+
46+
/** Query planning failed */
47+
PLANNING_ERROR,
48+
49+
/** Query execution failed */
50+
EXECUTION_ERROR,
51+
52+
/**
53+
* Unknown or unclassified error -- don't set this manually, it's filled in as the default if no
54+
* other code applies.
55+
*/
56+
UNKNOWN
57+
}
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.common.error;
7+
8+
import java.util.ArrayList;
9+
import java.util.LinkedHashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import lombok.Getter;
13+
14+
/**
15+
* Error report that wraps exceptions and accumulates contextual information as errors bubble up
16+
* through system layers.
17+
*
18+
* <p>Inspired by Rust's anyhow/eyre libraries, this class allows each layer to add context without
19+
* modifying the original exception message.
20+
*
21+
* <p>Example usage:
22+
*
23+
* <pre>
24+
* try {
25+
* resolveField(fieldName);
26+
* } catch (IllegalArgumentException e) {
27+
* throw ErrorReport.wrap(e)
28+
* .code(ErrorCode.FIELD_NOT_FOUND)
29+
* .stage(QueryProcessingStage.ANALYZING)
30+
* .location("while resolving fields in the index mapping")
31+
* .suggestion("Did you mean: '" + suggestedField + "'?")
32+
* .context("index_pattern", indexPattern)
33+
* .context("position", cursorPosition)
34+
* .build();
35+
* }
36+
* </pre>
37+
*/
38+
public class ErrorReport extends RuntimeException {
39+
40+
@Getter private final Exception cause;
41+
@Getter private final ErrorCode code;
42+
@Getter private final QueryProcessingStage stage;
43+
private final List<String> locationChain;
44+
private final Map<String, Object> context;
45+
@Getter private final String suggestion;
46+
@Getter private final String details;
47+
48+
private ErrorReport(Builder builder) {
49+
super(builder.cause.getMessage(), builder.cause);
50+
this.cause = builder.cause;
51+
this.code = builder.code;
52+
this.stage = builder.stage;
53+
this.locationChain = new ArrayList<>(builder.locationChain);
54+
this.context = new LinkedHashMap<>(builder.context);
55+
this.suggestion = builder.suggestion;
56+
this.details = builder.details;
57+
}
58+
59+
/**
60+
* Wraps an exception with an error report builder. If the exception is already an ErrorReport,
61+
* returns a builder initialized with the existing report's data.
62+
*
63+
* @param cause The underlying exception
64+
* @return A builder for constructing the error report
65+
*/
66+
public static Builder wrap(Exception cause) {
67+
if (cause instanceof ErrorReport existing) {
68+
return new Builder(existing.cause)
69+
.code(existing.code)
70+
.stage(existing.stage)
71+
.details(existing.details)
72+
.suggestion(existing.suggestion)
73+
.addLocationChain(existing.locationChain)
74+
.addContext(existing.context);
75+
}
76+
return new Builder(cause);
77+
}
78+
79+
public List<String> getLocationChain() {
80+
return new ArrayList<>(locationChain);
81+
}
82+
83+
public Map<String, Object> getContext() {
84+
return new LinkedHashMap<>(context);
85+
}
86+
87+
/** Get the original exception type name. */
88+
public String getExceptionType() {
89+
return cause.getClass().getSimpleName();
90+
}
91+
92+
/**
93+
* Format as a detailed message with all context information. This is suitable for logging or
94+
* detailed error displays.
95+
*/
96+
public String toDetailedMessage() {
97+
StringBuilder sb = new StringBuilder();
98+
99+
sb.append("Error");
100+
if (code != null && code != ErrorCode.UNKNOWN) {
101+
sb.append(" [").append(code).append("]");
102+
}
103+
if (stage != null) {
104+
sb.append(" at stage: ").append(stage.getDisplayName());
105+
}
106+
sb.append("\n");
107+
108+
if (details != null) {
109+
sb.append("Details: ").append(details).append("\n");
110+
}
111+
112+
if (!locationChain.isEmpty()) {
113+
sb.append("\nLocation chain:\n");
114+
for (int i = 0; i < locationChain.size(); i++) {
115+
// The location chain is typically appended to as we traverse up the stack, but for reading
116+
// the error it makes more sense to go down the stack. So we reverse it.
117+
sb.append(" ")
118+
.append(i + 1)
119+
.append(". ")
120+
.append(locationChain.get(locationChain.size() - i - 1))
121+
.append("\n");
122+
}
123+
}
124+
125+
if (!context.isEmpty()) {
126+
sb.append("\nContext:\n");
127+
context.forEach(
128+
(key, value) -> sb.append(" ").append(key).append(": ").append(value).append("\n"));
129+
}
130+
131+
if (suggestion != null) {
132+
sb.append("\nSuggestion: ").append(suggestion).append("\n");
133+
}
134+
135+
return sb.toString();
136+
}
137+
138+
/**
139+
* Convert to JSON-compatible map structure for REST API responses.
140+
*
141+
* @return Map containing error information in structured format
142+
*/
143+
public Map<String, Object> toJsonMap() {
144+
Map<String, Object> json = new LinkedHashMap<>();
145+
146+
json.put("type", getExceptionType());
147+
148+
if (code != null) {
149+
json.put("code", code.name());
150+
}
151+
152+
if (details != null) {
153+
json.put("details", details);
154+
}
155+
156+
if (!locationChain.isEmpty()) {
157+
// The location chain is typically appended to as we traverse up the stack, but for reading
158+
// the error it makes more sense to go down the stack. So we reverse it.
159+
json.put("location", locationChain.reversed());
160+
}
161+
162+
// Build context with stage information included
163+
Map<String, Object> contextMap = new LinkedHashMap<>(context);
164+
if (stage != null) {
165+
contextMap.put("stage", stage.toJsonKey());
166+
contextMap.put("stage_description", stage.getDisplayName());
167+
}
168+
if (!contextMap.isEmpty()) {
169+
json.put("context", contextMap);
170+
}
171+
172+
if (suggestion != null) {
173+
json.put("suggestion", suggestion);
174+
}
175+
176+
return json;
177+
}
178+
179+
/** Builder for constructing error reports with contextual information. */
180+
public static class Builder {
181+
private final Exception cause;
182+
private ErrorCode code = ErrorCode.UNKNOWN;
183+
private QueryProcessingStage stage = null;
184+
private final List<String> locationChain = new ArrayList<>();
185+
private final Map<String, Object> context = new LinkedHashMap<>();
186+
private String suggestion = null;
187+
private String details = null;
188+
189+
private Builder(Exception cause) {
190+
this.cause = cause;
191+
// Default details to the original exception message
192+
this.details =
193+
cause.getLocalizedMessage() != null ? cause.getLocalizedMessage() : cause.getMessage();
194+
}
195+
196+
/** Set the machine-readable error code. */
197+
public Builder code(ErrorCode code) {
198+
this.code = code;
199+
return this;
200+
}
201+
202+
/** Set the query processing stage where the error occurred. */
203+
public Builder stage(QueryProcessingStage stage) {
204+
// Don't overwrite more-specific stages with less-specific ones
205+
if (this.stage == null) {
206+
this.stage = stage;
207+
}
208+
return this;
209+
}
210+
211+
/**
212+
* Add a location to the chain describing where the error occurred. Locations are added in order
213+
* from innermost to outermost layer.
214+
*
215+
* @param location Description like "while resolving fields in index mapping"
216+
*/
217+
public Builder location(String location) {
218+
this.locationChain.add(location);
219+
return this;
220+
}
221+
222+
/**
223+
* Add multiple locations from an existing chain.
224+
*
225+
* @param locations List of location descriptions
226+
*/
227+
private Builder addLocationChain(List<String> locations) {
228+
this.locationChain.addAll(locations);
229+
return this;
230+
}
231+
232+
/**
233+
* Add structured context data (index name, query, position, etc).
234+
*
235+
* @param key Context key
236+
* @param value Context value (will be converted to string for serialization)
237+
*/
238+
public Builder context(String key, Object value) {
239+
this.context.put(key, value);
240+
return this;
241+
}
242+
243+
/**
244+
* Add multiple context entries from an existing map.
245+
*
246+
* @param contextMap Map of context key-value pairs
247+
*/
248+
private Builder addContext(Map<String, Object> contextMap) {
249+
this.context.putAll(contextMap);
250+
return this;
251+
}
252+
253+
/**
254+
* Set a suggestion for how to fix the error.
255+
*
256+
* @param suggestion User-facing suggestion like "Did you mean: 'foo'?"
257+
*/
258+
public Builder suggestion(String suggestion) {
259+
this.suggestion = suggestion;
260+
return this;
261+
}
262+
263+
/**
264+
* Override the default details message. By default, uses the wrapped exception's message.
265+
*
266+
* @param details Custom details message
267+
*/
268+
public Builder details(String details) {
269+
this.details = details;
270+
return this;
271+
}
272+
273+
/**
274+
* Build and throw the error report as an exception.
275+
*
276+
* @return The constructed error report (can be thrown)
277+
*/
278+
public ErrorReport build() {
279+
return new ErrorReport(this);
280+
}
281+
}
282+
}

0 commit comments

Comments
 (0)