001/* 002 * Copyright (C) 2022 - 2024, the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.github.ascopes.jct.diagnostics; 017 018import static java.util.Objects.requireNonNull; 019 020import io.github.ascopes.jct.utils.LoomPolyfill; 021import io.github.ascopes.jct.utils.VisibleForTestingOnly; 022import java.time.Instant; 023import java.util.List; 024import java.util.Locale; 025import java.util.concurrent.ConcurrentLinkedQueue; 026import java.util.function.Supplier; 027import java.util.stream.Collectors; 028import javax.tools.Diagnostic; 029import javax.tools.DiagnosticListener; 030import javax.tools.JavaFileObject; 031import org.apiguardian.api.API; 032import org.apiguardian.api.API.Status; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035import org.slf4j.event.Level; 036 037/** 038 * A diagnostics listener that wraps all diagnostics in additional invocation information, and then 039 * stores them in a queue for processing later. 040 * 041 * @param <S> the file type. 042 * @author Ashley Scopes 043 * @since 0.0.1 044 */ 045@API(since = "0.0.1", status = Status.STABLE) 046public class TracingDiagnosticListener<S extends JavaFileObject> implements DiagnosticListener<S> { 047 048 private final ConcurrentLinkedQueue<TraceDiagnostic<S>> diagnostics; 049 private final Logger logger; 050 private final Supplier<? extends Thread> threadGetter; 051 private final boolean logging; 052 private final boolean stackTraces; 053 054 /** 055 * Initialize this listener. 056 * 057 * @param logging {@code true} if logging is enabled, {@code false} otherwise. 058 * @param stackTraces {@code true} if logging stack traces is enabled, {@code false} otherwise. 059 * This is ignored if {@code logging} is {@code false}. 060 */ 061 public TracingDiagnosticListener( 062 boolean logging, 063 boolean stackTraces 064 ) { 065 this( 066 LoggerFactory.getLogger(TracingDiagnosticListener.class), 067 Thread::currentThread, 068 logging, 069 stackTraces 070 ); 071 } 072 073 /** 074 * Only visible for testing. 075 * 076 * @param logger the logger to use. 077 * @param threadGetter the supplier of the current thread. 078 * @param logging whether to enable logging. 079 * @param stackTraces whether to enable stack traces in the logging. 080 */ 081 @API(since = "0.0.1", status = Status.INTERNAL) 082 @VisibleForTestingOnly 083 protected TracingDiagnosticListener( 084 Logger logger, 085 Supplier<? extends Thread> threadGetter, 086 boolean logging, 087 boolean stackTraces 088 ) { 089 diagnostics = new ConcurrentLinkedQueue<>(); 090 this.logger = requireNonNull(logger, "logger"); 091 this.threadGetter = requireNonNull(threadGetter, "threadGetter"); 092 this.logging = logging; 093 this.stackTraces = stackTraces; 094 } 095 096 /** 097 * Determine if logging is enabled. 098 * 099 * @return {@code true} if enabled, or {@code false} if disabled. 100 */ 101 public boolean isLoggingEnabled() { 102 return logging; 103 } 104 105 /** 106 * Determine if stack trace reporting is enabled. 107 * 108 * <p>Note that stack traces are only reported if {@link #isLoggingEnabled() logging is enabled} 109 * as well. 110 * 111 * @return {@code true} if enabled, or {@code false} if disabled. 112 */ 113 public boolean isStackTraceReportingEnabled() { 114 return stackTraces; 115 } 116 117 /** 118 * Get a copy of the queue containing all the diagnostics that have been detected. 119 * 120 * @return the diagnostics in a list. 121 */ 122 public List<TraceDiagnostic<S>> getDiagnostics() { 123 return List.copyOf(diagnostics); 124 } 125 126 @Override 127 public final void report(Diagnostic<? extends S> diagnostic) { 128 requireNonNull(diagnostic); 129 130 var now = Instant.now(); 131 var thisThread = threadGetter.get(); 132 var threadName = thisThread.getName(); 133 var stackTrace = List.of(thisThread.getStackTrace()); 134 var threadId = LoomPolyfill.getThreadId(thisThread); 135 136 var wrapped = new TraceDiagnostic<S>(now, threadId, threadName, stackTrace, diagnostic); 137 138 diagnostics.add(wrapped); 139 140 if (!logging) { 141 return; 142 } 143 144 logger 145 .atLevel(diagnosticToLevel(diagnostic)) 146 .setMessage("{}{}") 147 .addArgument(messageGetter(wrapped)) 148 .addArgument(stackTraceFormatter(stackTrace)) 149 .log(); 150 } 151 152 private Level diagnosticToLevel(Diagnostic<?> diagnostic) { 153 switch (diagnostic.getKind()) { 154 case ERROR: 155 return Level.ERROR; 156 case WARNING: 157 case MANDATORY_WARNING: 158 return Level.WARN; 159 default: 160 return Level.INFO; 161 } 162 } 163 164 private Supplier<String> messageGetter(Diagnostic<?> diagnostic) { 165 return () -> diagnostic.getMessage(Locale.ROOT); 166 } 167 168 private Supplier<String> stackTraceFormatter(List<StackTraceElement> stackTrace) { 169 if (!stackTraces) { 170 return () -> ""; 171 } 172 173 return () -> stackTrace 174 .stream() 175 .map(frame -> "\n\t" + frame) 176 .collect(Collectors.joining()); 177 } 178}