package org.stagemonitor.web.monitor.widget; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.stagemonitor.core.util.Pair; import org.stagemonitor.tracing.SpanContextInformation; import org.stagemonitor.tracing.reporter.ReadbackSpan; import org.stagemonitor.tracing.reporter.SpanReporter; import org.stagemonitor.tracing.wrapper.AbstractSpanEventListener; import org.stagemonitor.tracing.wrapper.SpanEventListener; import org.stagemonitor.tracing.wrapper.SpanEventListenerFactory; import org.stagemonitor.tracing.wrapper.SpanWrapper; import org.stagemonitor.web.monitor.MonitoredHttpRequest; import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import io.opentracing.Span; import static org.stagemonitor.web.monitor.MonitoredHttpRequest.WIDGET_ALLOWED_ATTRIBUTE; public class WidgetAjaxSpanReporter extends SpanReporter { public static final String CONNECTION_ID = "x-stagemonitor-connection-id"; private static final long MAX_REQUEST_TRACE_BUFFERING_TIME = 60 * 1000; private final Logger logger = LoggerFactory.getLogger(getClass()); private ConcurrentMap<String, ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>>> connectionIdToSpanMap = new ConcurrentHashMap<String, ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>>>(); private ConcurrentMap<String, Object> connectionIdToLockMap = new ConcurrentHashMap<String, Object>(); /** * see {@link OldSpanRemover} */ private ScheduledExecutorService oldSpanRemoverPool; public WidgetAjaxSpanReporter() { } public void init() { oldSpanRemoverPool = Executors.newScheduledThreadPool(1, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setName("request-trace-remover"); return thread; } }); oldSpanRemoverPool.scheduleAtFixedRate(new OldSpanRemover(), MAX_REQUEST_TRACE_BUFFERING_TIME, MAX_REQUEST_TRACE_BUFFERING_TIME, TimeUnit.MILLISECONDS); } Collection<Pair<Long, ReadbackSpan>> getSpans(String connectionId, long requestTimeout) throws IOException { if (connectionId != null && !connectionId.trim().isEmpty()) { final ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>> traces = connectionIdToSpanMap.remove(connectionId); if (traces != null) { logger.debug("picking up buffered requests"); return traces; } else { return blockingWaitForSpan(connectionId, requestTimeout); } } else { throw new IllegalArgumentException("connectionId is empty"); } } private ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>> blockingWaitForSpan(String connectionId, Long requestTimeout) throws IOException { Object lock = new Object(); synchronized (lock) { connectionIdToLockMap.put(connectionId, lock); try { lock.wait(requestTimeout); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { connectionIdToLockMap.remove(connectionId, lock); } return connectionIdToSpanMap.remove(connectionId); } } @Override public void report(SpanContextInformation spanContext) throws IOException { if (isActive(spanContext)) { final String connectionId = (String) spanContext.getRequestAttributes().get(MonitoredHttpRequest.CONNECTION_ID_ATTRIBUTE); if (connectionId != null && !connectionId.trim().isEmpty()) { logger.debug("buffering span '{}'", spanContext.getOperationName()); bufferSpan(connectionId, spanContext.getReadbackSpan()); final Object lock = connectionIdToLockMap.remove(connectionId); if (lock != null) { synchronized (lock) { lock.notifyAll(); } } } } } private void bufferSpan(String connectionId, ReadbackSpan span) { logger.debug("bufferSpan {}", span); ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>> httpSpans = new ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>>(); httpSpans.add(Pair.of(System.currentTimeMillis(), span)); final ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>> alreadyAssociatedValue = connectionIdToSpanMap .putIfAbsent(connectionId, httpSpans); if (alreadyAssociatedValue != null) { alreadyAssociatedValue.add(Pair.of(System.currentTimeMillis(), span)); } } @Override public boolean isActive(SpanContextInformation spanContext) { return Boolean.TRUE.equals(spanContext.getRequestAttributes().get(WIDGET_ALLOWED_ATTRIBUTE)); } /** * Clears old {@link Span}s that are buffered in {@link #connectionIdToSpanMap} but are never picked up * to prevent a memory leak */ private class OldSpanRemover implements Runnable { @Override public void run() { for (Map.Entry<String, ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>>> entry : connectionIdToSpanMap.entrySet()) { final ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>> httpSpans = entry.getValue(); removeOldSpans(httpSpans); if (httpSpans.isEmpty()) { removeOrphanEntry(entry); } } } private void removeOldSpans(ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>> httpSpans) { for (Iterator<Pair<Long, ReadbackSpan>> iterator = httpSpans.iterator(); iterator.hasNext(); ) { Pair<Long, ReadbackSpan> httpSpan = iterator.next(); final long timeInBuffer = System.currentTimeMillis() - httpSpan.getA(); if (timeInBuffer > MAX_REQUEST_TRACE_BUFFERING_TIME) { iterator.remove(); } } } private void removeOrphanEntry(Map.Entry<String, ConcurrentLinkedQueue<Pair<Long, ReadbackSpan>>> entry) { // to eliminate race conditions remove only if queue is still empty connectionIdToSpanMap.remove(entry.getKey(), new ConcurrentLinkedQueue<Span>()); } } public void close() { oldSpanRemoverPool.shutdown(); } public static class WidgetAllowedEventListener extends AbstractSpanEventListener { private boolean widgetAllowed = false; @Override public void onStart(SpanWrapper spanWrapper) { SpanContextInformation.forSpan(spanWrapper).addRequestAttribute(WIDGET_ALLOWED_ATTRIBUTE, widgetAllowed); } @Override public String onSetTag(String key, String value) { if (WIDGET_ALLOWED_ATTRIBUTE.equals(key)) { widgetAllowed = true; } return value; } public static class Factory implements SpanEventListenerFactory { @Override public SpanEventListener create() { return new WidgetAllowedEventListener(); } } } }