3333import java .lang .reflect .InvocationTargetException ;
3434import java .lang .reflect .Method ;
3535import 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 ;
3739import java .util .*;
3840import java .util .concurrent .TimeUnit ;
3941import 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}
0 commit comments