Skip to content

Commit 0f63e35

Browse files
committed
Initial changes for reporting interface
Signed-off-by: Simeon Widdis <sawiddis@amazon.com>
1 parent 019cd26 commit 0f63e35

8 files changed

Lines changed: 725 additions & 4 deletions

File tree

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

0 commit comments

Comments
 (0)