Skip to content

Commit 143894d

Browse files
committed
otel: jsonrpc: implement instrumentation
- fix lifecycle of request - add tracing
1 parent 642fe97 commit 143894d

13 files changed

Lines changed: 281 additions & 140 deletions

File tree

modules/jooby-jsonrpc/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,11 @@
1919
<version>${jooby.version}</version>
2020
</dependency>
2121

22+
<dependency>
23+
<groupId>io.opentelemetry</groupId>
24+
<artifactId>opentelemetry-api</artifactId>
25+
<version>${opentelemetry.version}</version>
26+
<optional>true</optional>
27+
</dependency>
2228
</dependencies>
2329
</project>

modules/jooby-jsonrpc/src/main/java/io/jooby/internal/jsonrpc/DefaultJsonRpcInvoker.java

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jsonrpc;
7+
8+
import java.util.Map;
9+
import java.util.Optional;
10+
11+
import io.jooby.Reified;
12+
import io.jooby.Router;
13+
import io.jooby.jsonrpc.JsonRpcErrorCode;
14+
import io.jooby.jsonrpc.JsonRpcResponse;
15+
16+
public class JsonRpcExceptionTranslator {
17+
private final Router router;
18+
19+
public JsonRpcExceptionTranslator(Router router) {
20+
this.router = router;
21+
}
22+
23+
public JsonRpcErrorCode toErrorCode(Throwable cause) {
24+
// Attempt to look up any user-defined exception mappings from the registry
25+
Map<Class<?>, JsonRpcErrorCode> customMapping =
26+
router.require(Reified.map(Class.class, JsonRpcErrorCode.class));
27+
return errorCode(customMapping, cause)
28+
.orElseGet(() -> JsonRpcErrorCode.of(router.errorCode(cause)));
29+
}
30+
31+
public JsonRpcResponse.ErrorDetail toErrorDetail(Throwable cause) {
32+
return new JsonRpcResponse.ErrorDetail(toErrorCode(cause), cause);
33+
}
34+
35+
/**
36+
* Evaluates the given exception against the registered custom exception mappings.
37+
*
38+
* @param mappings A map of Exception classes to specific tRPC error codes.
39+
* @param cause The exception to evaluate.
40+
* @return An {@code Optional} containing the matched {@code TrpcErrorCode}, or empty if no match
41+
* is found.
42+
*/
43+
private Optional<JsonRpcErrorCode> errorCode(
44+
Map<Class<?>, JsonRpcErrorCode> mappings, Throwable cause) {
45+
for (var mapping : mappings.entrySet()) {
46+
if (mapping.getKey().isInstance(cause)) {
47+
return Optional.of(mapping.getValue());
48+
}
49+
}
50+
return Optional.empty();
51+
}
52+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jsonrpc;
7+
8+
import java.util.Map;
9+
import java.util.Optional;
10+
11+
import io.jooby.Context;
12+
import io.jooby.SneakyThrows;
13+
import io.jooby.jsonrpc.JsonRpcErrorCode;
14+
import io.jooby.jsonrpc.JsonRpcRequest;
15+
import io.jooby.jsonrpc.JsonRpcResponse;
16+
import io.jooby.jsonrpc.JsonRpcService;
17+
18+
public class JsonRpcExecutor implements SneakyThrows.Supplier<Optional<JsonRpcResponse>> {
19+
private final Map<String, JsonRpcService> services;
20+
private final Context ctx;
21+
private final JsonRpcRequest request;
22+
23+
public JsonRpcExecutor(
24+
Map<String, JsonRpcService> services, Context ctx, JsonRpcRequest request) {
25+
this.services = services;
26+
this.ctx = ctx;
27+
this.request = request;
28+
}
29+
30+
@Override
31+
public Optional<JsonRpcResponse> tryGet() throws Exception {
32+
var fullMethod = request.getMethod();
33+
if (fullMethod == null) {
34+
return Optional.of(
35+
JsonRpcResponse.error(request.getId(), JsonRpcErrorCode.INVALID_REQUEST, null));
36+
}
37+
var targetService = services.get(fullMethod);
38+
if (targetService != null) {
39+
var result = targetService.execute(ctx, request);
40+
return request.getId() != null
41+
? Optional.of(JsonRpcResponse.success(request.getId(), result))
42+
: Optional.empty();
43+
}
44+
return Optional.of(
45+
JsonRpcResponse.error(
46+
request.getId(), JsonRpcErrorCode.METHOD_NOT_FOUND, "Method not found: " + fullMethod));
47+
}
48+
}

modules/jooby-jsonrpc/src/main/java/io/jooby/jsonrpc/JsonRpcException.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
*/
66
package io.jooby.jsonrpc;
77

8+
import java.util.Optional;
9+
10+
import org.jspecify.annotations.Nullable;
11+
812
/**
913
* Exception thrown when a JSON-RPC error occurs during routing, parsing, or execution.
1014
*
@@ -14,7 +18,7 @@
1418
public class JsonRpcException extends RuntimeException {
1519
private final JsonRpcErrorCode code;
1620

17-
private final Object data;
21+
private final @Nullable Object data;
1822

1923
/**
2024
* Constructs a new JSON-RPC exception.
@@ -68,7 +72,12 @@ public JsonRpcErrorCode getCode() {
6872
*
6973
* @return Additional data regarding the error, or null if none was provided.
7074
*/
71-
public Object getData() {
75+
public @Nullable Object getData() {
7276
return data;
7377
}
78+
79+
public JsonRpcResponse.ErrorDetail toErrorDetail() {
80+
return new JsonRpcResponse.ErrorDetail(
81+
code, getMessage(), Optional.ofNullable(data).orElse(getCause()));
82+
}
7483
}

modules/jooby-jsonrpc/src/main/java/io/jooby/jsonrpc/JsonRpcInvoker.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,20 @@
66
package io.jooby.jsonrpc;
77

88
import java.util.Objects;
9+
import java.util.Optional;
910

1011
import io.jooby.Context;
1112
import io.jooby.SneakyThrows;
1213

1314
public interface JsonRpcInvoker {
1415

15-
Object invoke(Context ctx, JsonRpcRequest request, SneakyThrows.Supplier<Object> action)
16+
Optional<JsonRpcResponse> invoke(
17+
Context ctx, JsonRpcRequest request, SneakyThrows.Supplier<Optional<JsonRpcResponse>> action)
1618
throws Exception;
1719

1820
default JsonRpcInvoker then(JsonRpcInvoker next) {
1921
Objects.requireNonNull(next, "next invoker is required");
20-
return new JsonRpcInvoker() {
21-
@Override
22-
public Object invoke(
23-
Context ctx, JsonRpcRequest request, SneakyThrows.Supplier<Object> action)
24-
throws Exception {
25-
return JsonRpcInvoker.this.invoke(ctx, request, () -> next.invoke(ctx, request, action));
26-
}
27-
};
22+
return (ctx, request, action) ->
23+
JsonRpcInvoker.this.invoke(ctx, request, () -> next.invoke(ctx, request, action));
2824
}
2925
}

modules/jooby-jsonrpc/src/main/java/io/jooby/jsonrpc/JsonRpcModule.java

Lines changed: 38 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
import io.jooby.*;
1515
import io.jooby.exception.MissingValueException;
1616
import io.jooby.exception.TypeMismatchException;
17+
import io.jooby.internal.jsonrpc.JsonRpcExceptionTranslator;
18+
import io.jooby.internal.jsonrpc.JsonRpcExecutor;
19+
import io.jooby.jsonrpc.instrumentation.OtelJsonRcpTracing;
1720

1821
/**
1922
* Global Tier 1 Dispatcher for JSON-RPC 2.0 requests.
@@ -52,6 +55,8 @@ public class JsonRpcModule implements Extension {
5255
private final Map<String, JsonRpcService> services = new HashMap<>();
5356
private final String path;
5457
private @Nullable JsonRpcInvoker invoker;
58+
private @Nullable OtelJsonRcpTracing head;
59+
private JsonRpcExceptionTranslator exceptionTranslator;
5560

5661
public JsonRpcModule(String path, JsonRpcService service, JsonRpcService... services) {
5762
this.path = path;
@@ -64,10 +69,15 @@ public JsonRpcModule(JsonRpcService service, JsonRpcService... services) {
6469
}
6570

6671
public JsonRpcModule invoker(JsonRpcInvoker invoker) {
67-
if (this.invoker != null) {
68-
this.invoker = invoker.then(this.invoker);
72+
if (invoker instanceof OtelJsonRcpTracing otel) {
73+
// otel goes first:
74+
this.head = otel;
6975
} else {
70-
this.invoker = invoker;
76+
if (this.invoker != null) {
77+
this.invoker = invoker.then(this.invoker);
78+
} else {
79+
this.invoker = invoker;
80+
}
7181
}
7282
return this;
7383
}
@@ -86,8 +96,13 @@ private void registry(JsonRpcService service) {
8696
*/
8797
@Override
8898
public void install(Jooby app) throws Exception {
99+
if (head != null) {
100+
invoker = invoker == null ? head : head.then(invoker);
101+
}
89102
app.post(path, this::handle);
90103

104+
exceptionTranslator = new JsonRpcExceptionTranslator(app);
105+
app.getServices().put(JsonRpcExceptionTranslator.class, exceptionTranslator);
91106
// Initialize the custom exception mapping registry
92107
app.getServices()
93108
.mapOf(Class.class, JsonRpcErrorCode.class)
@@ -106,64 +121,43 @@ public void install(Jooby app) throws Exception {
106121
* @return A single {@link JsonRpcResponse}, a {@code List} of responses for batches, or an empty
107122
* string for notifications.
108123
*/
109-
private Object handle(Context ctx) {
124+
private Object handle(Context ctx) throws Exception {
110125
JsonRpcRequest input;
111126
try {
112127
input = ctx.body(JsonRpcRequest.class);
113-
} catch (Exception e) {
114-
// Spec: -32700 Parse error if the JSON is physically malformed.
115-
return JsonRpcResponse.error(null, JsonRpcErrorCode.PARSE_ERROR, e);
128+
} catch (Exception cause) {
129+
var badRequest = new JsonRpcRequest();
130+
badRequest.setMethod(JsonRpcRequest.UNKNOWN_METHOD);
131+
var parseError = JsonRpcResponse.error(null, JsonRpcErrorCode.PARSE_ERROR, cause);
132+
if (head != null) {
133+
// Manually handle bad request for otel
134+
return head.invoke(ctx, badRequest, () -> Optional.of(parseError));
135+
}
136+
log(badRequest, cause);
137+
return parseError;
116138
}
117139

118140
List<JsonRpcResponse> responses = new ArrayList<>();
119141

120142
// Look up all generated *Rpc classes registered in the service registry
121-
122143
for (var request : input) {
123-
var fullMethod = request.getMethod();
124-
125-
// Spec: -32600 Invalid Request if the method member is missing or null
126-
if (fullMethod == null) {
127-
responses.add(
128-
JsonRpcResponse.error(request.getId(), JsonRpcErrorCode.INVALID_REQUEST, null));
129-
continue;
130-
}
131-
132144
try {
133-
var targetService = services.get(fullMethod);
134-
if (targetService != null) {
135-
var result =
136-
invoker == null
137-
? targetService.execute(ctx, request)
138-
: invoker.invoke(ctx, request, () -> targetService.execute(ctx, request));
139-
// Spec: If the "id" is missing, it is a notification and no response is returned.
140-
if (request.getId() != null) {
141-
if (result instanceof JsonRpcResponse jsonRpcResponse) {
142-
responses.add(jsonRpcResponse);
143-
} else {
144-
responses.add(JsonRpcResponse.success(request.getId(), result));
145-
}
146-
}
147-
} else {
148-
// Spec: -32601 Method not found
149-
responses.add(
150-
JsonRpcResponse.error(
151-
request.getId(),
152-
JsonRpcErrorCode.METHOD_NOT_FOUND,
153-
"Method not found: " + fullMethod));
154-
}
145+
var target = new JsonRpcExecutor(services, ctx, request);
146+
var response = invoker == null ? target.get() : invoker.invoke(ctx, request, target);
147+
response.ifPresent(responses::add);
155148
} catch (JsonRpcException cause) {
156-
log(ctx, request, cause);
149+
log(request, cause);
157150
// Domain-specific or protocol-level exceptions (e.g., -32602 Invalid Params)
158151
if (request.getId() != null) {
159152
responses.add(JsonRpcResponse.error(request.getId(), cause.getCode(), cause.getCause()));
160153
}
161154
} catch (Exception cause) {
162-
log(ctx, request, cause);
155+
log(request, cause);
163156
// Spec: -32603 Internal error for unhandled application exceptions
164157
if (request.getId() != null) {
165158
responses.add(
166-
JsonRpcResponse.error(request.getId(), computeErrorCode(ctx, cause), cause));
159+
JsonRpcResponse.error(
160+
request.getId(), exceptionTranslator.toErrorCode(cause), cause));
167161
}
168162
}
169163
}
@@ -178,14 +172,14 @@ private Object handle(Context ctx) {
178172
return input.isBatch() ? responses : responses.getFirst();
179173
}
180174

181-
private void log(Context ctx, JsonRpcRequest request, Throwable cause) {
175+
private void log(JsonRpcRequest request, Throwable cause) {
182176
JsonRpcErrorCode code;
183177
boolean hasCause = true;
184178
if (cause instanceof JsonRpcException rpcException) {
185179
code = rpcException.getCode();
186180
hasCause = false;
187181
} else {
188-
code = computeErrorCode(ctx, cause);
182+
code = exceptionTranslator.toErrorCode(cause);
189183
}
190184
var type = code == JsonRpcErrorCode.INTERNAL_ERROR ? "server" : "client";
191185
var message = "JSON-RPC {} error [{} {}] on method '{}' (id: {})";
@@ -230,33 +224,4 @@ private void log(Context ctx, JsonRpcRequest request, Throwable cause) {
230224
}
231225
}
232226
}
233-
234-
private JsonRpcErrorCode computeErrorCode(Context ctx, Throwable cause) {
235-
JsonRpcErrorCode code;
236-
// Attempt to look up any user-defined exception mappings from the registry
237-
Map<Class<?>, JsonRpcErrorCode> customMapping =
238-
ctx.require(Reified.map(Class.class, JsonRpcErrorCode.class));
239-
code =
240-
errorCode(customMapping, cause)
241-
.orElseGet(() -> JsonRpcErrorCode.of(ctx.getRouter().errorCode(cause)));
242-
return code;
243-
}
244-
245-
/**
246-
* Evaluates the given exception against the registered custom exception mappings.
247-
*
248-
* @param mappings A map of Exception classes to specific tRPC error codes.
249-
* @param x The exception to evaluate.
250-
* @return An {@code Optional} containing the matched {@code TrpcErrorCode}, or empty if no match
251-
* is found.
252-
*/
253-
private Optional<JsonRpcErrorCode> errorCode(
254-
Map<Class<?>, JsonRpcErrorCode> mappings, Throwable x) {
255-
for (var mapping : mappings.entrySet()) {
256-
if (mapping.getKey().isInstance(x)) {
257-
return Optional.of(mapping.getValue());
258-
}
259-
}
260-
return Optional.empty();
261-
}
262227
}

modules/jooby-jsonrpc/src/main/java/io/jooby/jsonrpc/JsonRpcRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* generic structure (e.g., a List or a Map) and populating the batch state.
3434
*/
3535
public class JsonRpcRequest implements Iterable<JsonRpcRequest> {
36+
public static final String UNKNOWN_METHOD = "unknown_method";
3637

3738
/** A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". */
3839
private String jsonrpc = "2.0";

0 commit comments

Comments
 (0)