Skip to content

Commit c3fc4f7

Browse files
authored
fix analytics error handling (#150)
Signed-off-by: shalom <[email protected]> Signed-off-by: shalom <[email protected]>
1 parent 00bb1bd commit c3fc4f7

File tree

3 files changed

+75
-35
lines changed

3 files changed

+75
-35
lines changed

ide-common/src/main/java/org/digma/intellij/plugin/analytics/AnalyticsService.java

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import java.lang.reflect.InvocationTargetException;
3434
import java.lang.reflect.Method;
3535
import java.lang.reflect.UndeclaredThrowableException;
36-
import java.net.ConnectException;
36+
import java.net.SocketException;
37+
import java.net.UnknownHostException;
38+
import java.net.http.HttpTimeoutException;
3739
import java.util.*;
3840
import java.util.concurrent.TimeUnit;
3941
import java.util.function.Supplier;
@@ -52,6 +54,9 @@ public class AnalyticsService implements Disposable {
5254

5355
private AnalyticsProvider analyticsProviderProxy;
5456

57+
//this status is used to keep track of connection errors, and for helping with reporting messages only when necessary
58+
// and keep the log clean
59+
private final Status status = new Status();
5560

5661
public AnalyticsService(@NotNull Project project) {
5762
//initialize BackendConnectionMonitor when starting, so it is aware early on connection statuses
@@ -91,11 +96,11 @@ private synchronized void replaceClient(String url, String token) {
9196
try {
9297
analyticsProviderProxy.close();
9398
} catch (IOException e) {
94-
e.printStackTrace();
99+
Log.log(LOGGER::warn, e.getMessage());
95100
}
96101
}
97102

98-
analyticsProviderProxy = newAnalyticsProviderProxy(new RestAnalyticsProvider(url, token), project);
103+
analyticsProviderProxy = newAnalyticsProviderProxy(new RestAnalyticsProvider(url, token));
99104
}
100105

101106

@@ -133,8 +138,8 @@ List<String> getEnvironments() {
133138
try {
134139
return analyticsProviderProxy.getEnvironments();
135140
} catch (Exception e) {
136-
//getEnvironments should never throw exception. it is called only from the constructor or be the
137-
//Environment object that can handle null.
141+
//getEnvironments should never throw exception.
142+
// it is called only from this class or from the Environment object and both can handle null.
138143
return null;
139144
}
140145
}
@@ -218,25 +223,30 @@ private <T> T executeCatching(Supplier<T> tSupplier) throws AnalyticsServiceExce
218223
}
219224

220225

226+
private AnalyticsProvider newAnalyticsProviderProxy(AnalyticsProvider obj) {
227+
return (AnalyticsProvider) java.lang.reflect.Proxy.newProxyInstance(
228+
obj.getClass().getClassLoader(),
229+
new Class[]{AnalyticsProvider.class, Closeable.class},
230+
new AnalyticsInvocationHandler(obj));
231+
}
232+
233+
221234
/**
222235
* A proxy for cross-cutting concerns across all api methods.
223236
* In a proxy it's easier to log events, we have the method name, parameters etc.
224-
* easier to investigate exceptions, if its an InvocationTargetException or IllegalAccessException etc.
237+
* easier to investigate exceptions, if it's an InvocationTargetException or IllegalAccessException etc.
238+
* It's an inner class intentionally, so it has access to the enclosing AnalyticsService members.
225239
*/
226-
private static class AnalyticsInvocationHandler implements InvocationHandler {
240+
private class AnalyticsInvocationHandler implements InvocationHandler {
227241

228242
private final AnalyticsProvider analyticsProvider;
229-
private final Project project;
230243

231244
//ObjectMapper here is only used for printing the result to log as json
232245
private final ObjectMapper objectMapper = new ObjectMapper();
233246

234-
//this status is only used for helping with reporting messages only when necessary and keep the log clean
235-
private final Status status = new Status();
236247

237-
public AnalyticsInvocationHandler(AnalyticsProvider analyticsProvider,Project project) {
248+
public AnalyticsInvocationHandler(AnalyticsProvider analyticsProvider) {
238249
this.analyticsProvider = analyticsProvider;
239-
this.project = project;
240250
}
241251

242252

@@ -279,9 +289,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
279289

280290
} catch (InvocationTargetException e) {
281291

292+
//Note: when logging LOGGER.error idea will popup a red message which we don't want, so only report warn messages.
293+
282294
//handle only InvocationTargetException, other exceptions are probably a bug.
283-
//log to error only the first time of ConnectException and show an error notification.
284-
// following exceptions will just be logged to the log file
295+
//log connection exceptions only the first time and show an error notification.
296+
// while status is in error the following connection exceptions will not be logged, other exceptions
297+
// will be logged only once.
298+
285299
boolean isConnectionException = isConnectionException(e) || isSslConnectionException(e);
286300
if (status.isOk() && isConnectionException) {
287301
status.connectError();
@@ -293,14 +307,16 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
293307
+ message + ".<br> See logs for details.");
294308
} else if (status.isOk()) {
295309
status.error();
296-
Log.log(LOGGER::debug,"Error invoking AnalyticsProvider.{}({}), exception {}", method.getName(), argsToString(args), e.getCause().getMessage());
310+
Log.log(LOGGER::warn, "Error invoking AnalyticsProvider.{}({}), exception {}", method.getName(), argsToString(args), e.getCause().getMessage());
297311
var message = getExceptionMessage(e);
298312
NotificationUtil.notifyError(project, "<html>Error with Digma backend api for method " + method.getName() + ".<br> "
299313
+ message + ".<br> See logs for details.");
314+
LOGGER.warn(e);
300315
} else if (!status.hadError(e)) {
301316
status.error();
302317
var message = getExceptionMessage(e);
303-
Log.log(LOGGER::debug, "Error invoking AnalyticsProvider.{}({}), exception {}", method.getName(), argsToString(args), message);
318+
Log.log(LOGGER::warn, "Error invoking AnalyticsProvider.{}({}), exception {}", method.getName(), argsToString(args), message);
319+
LOGGER.warn(e);
304320
}
305321

306322

@@ -313,6 +329,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
313329
} catch (Throwable e) {
314330
status.error();
315331
Log.log(LOGGER::debug, "Error invoking AnalyticsProvider.{}({}), exception {}", method.getName(), argsToString(args), e.getMessage());
332+
LOGGER.error(e);
316333
throw e;
317334
} finally {
318335
stopWatch.stop();
@@ -353,7 +370,9 @@ private boolean isConnectionUnavailableException(Throwable exception) {
353370

354371
//InterruptedIOException is thrown when the connection is dropped , for example by iptables
355372

356-
return exception instanceof ConnectException ||
373+
return exception instanceof SocketException ||
374+
exception instanceof UnknownHostException ||
375+
exception instanceof HttpTimeoutException ||
357376
exception instanceof InterruptedIOException;
358377

359378
}
@@ -387,7 +406,7 @@ private String getExceptionMessage(InvocationTargetException e) {
387406
ex = ex.getCause();
388407
}
389408
if (ex != null){
390-
return ex.getMessage();
409+
return ex.getCause() != null ? ex.getCause().getMessage() : ex.getMessage();
391410
}
392411

393412
return e.getCause() != null? e.getCause().getMessage():e.getMessage();
@@ -415,15 +434,6 @@ private String argsToString(Object[] args){
415434
}
416435

417436

418-
419-
private AnalyticsProvider newAnalyticsProviderProxy(AnalyticsProvider obj, Project project) {
420-
return (AnalyticsProvider) java.lang.reflect.Proxy.newProxyInstance(
421-
obj.getClass().getClassLoader(),
422-
new Class[]{AnalyticsProvider.class, Closeable.class},
423-
new AnalyticsInvocationHandler(obj,project));
424-
}
425-
426-
427437
private static class Status {
428438

429439
private final Map<String, Boolean> errorsHistory = new HashMap<>();
@@ -459,17 +469,30 @@ public void error() {
459469
}
460470

461471
public boolean hadError(InvocationTargetException e) {
462-
var ex = e.getCause();
463-
if (ex != null){
464-
var errorName = ex.getClass().getName();
465-
if (errorsHistory.containsKey(errorName)){
466-
return true;
467-
}
468-
errorsHistory.put(errorName,true);
472+
var cause = findRealError(e);
473+
var errorName = cause.getClass().getName();
474+
if (errorsHistory.containsKey(errorName)) {
475+
return true;
469476
}
477+
errorsHistory.put(errorName, true);
470478

471479
return false;
472480
}
481+
482+
@NotNull
483+
private Throwable findRealError(InvocationTargetException e) {
484+
485+
Throwable cause = e.getCause();
486+
while (cause != null && !cause.getClass().equals(AnalyticsProviderException.class)) {
487+
cause = cause.getCause();
488+
}
489+
490+
if (cause != null && cause.getCause() != null) {
491+
return cause.getCause();
492+
}
493+
494+
return cause == null ? e : cause;
495+
}
473496
}
474497

475498
}

ide-common/src/main/java/org/digma/intellij/plugin/notifications/NotificationUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static void notifyChangingEnvironment(Project project, String oldEnv,Stri
2828

2929
public static void showNotification(Project project,String content) {
3030

31-
content = content.startsWith("content") ? content : "Digma: " +content;
31+
content = content.startsWith("Digma:") ? content : "Digma: " + content;
3232
NotificationGroupManager.getInstance()
3333
.getNotificationGroup(NOTIFICATION_GROUP)
3434
.createNotification(content, NotificationType.INFORMATION)

ide-common/src/main/kotlin/org/digma/intellij/plugin/ui/service/AbstractViewService.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,34 @@ abstract class AbstractViewService(val project: Project) {
2424
.subscribe(AnalyticsServiceConnectionEvent.ANALYTICS_SERVICE_CONNECTION_EVENT_TOPIC,
2525
handler = object : AnalyticsServiceConnectionEvent {
2626
override fun connectionLost() {
27+
doConnectionLost()
2728
doUpdateUi()
2829
}
2930

3031
override fun connectionGained() {
32+
doConnectionGained()
3133
doUpdateUi()
3234
}
3335
}
3436
)
3537
}
3638

3739

40+
fun doConnectionLost() {
41+
//if a view needs to do something when connection lost can override this method and don't forget to call super
42+
if (toolWindowTabsHelper.isErrorDetailsOn()) {
43+
toolWindowTabsHelper.errorDetailsOff()
44+
if (this is ErrorsViewService) {
45+
this.closeErrorDetails()
46+
}
47+
toolWindowTabsHelper.errorDetailsClosed()
48+
}
49+
}
50+
51+
fun doConnectionGained() {
52+
//if a view needs to do something when connection gained can override this method and don't forget to call super
53+
}
54+
3855
abstract fun getViewDisplayName(): String
3956

4057
//in some situation the UI should not be updated, for example if the error details is On then nothing changes

0 commit comments

Comments
 (0)