001/* 002 * Copyright (C) 2022 - 2025, 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.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033import org.slf4j.event.Level; 034 035/** 036 * A diagnostics listener that wraps all diagnostics in additional invocation information, and then 037 * stores them in a queue for processing later. 038 * 039 * @param <S> the file type. 040 * @author Ashley Scopes 041 * @since 0.0.1 042 */ 043public class TracingDiagnosticListener<S extends JavaFileObject> implements DiagnosticListener<S> { 044 045 private final ConcurrentLinkedQueue<TraceDiagnostic<S>> diagnostics; 046 private final Logger logger; 047 private final Supplier<? extends Thread> threadGetter; 048 private final boolean logging; 049 private final boolean stackTraces; 050 051 /** 052 * Initialize this listener. 053 * 054 * @param logging {@code true} if logging is enabled, {@code false} otherwise. 055 * @param stackTraces {@code true} if logging stack traces is enabled, {@code false} otherwise. 056 * This is ignored if {@code logging} is {@code false}. 057 */ 058 public TracingDiagnosticListener( 059 boolean logging, 060 boolean stackTraces 061 ) { 062 this( 063 LoggerFactory.getLogger(TracingDiagnosticListener.class), 064 Thread::currentThread, 065 logging, 066 stackTraces 067 ); 068 } 069 070 /** 071 * Only visible for testing. 072 * 073 * <p>Users should <strong>NOT</strong> use this constructor. It may be changed 074 * or removed without notice. 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 @VisibleForTestingOnly 082 protected TracingDiagnosticListener( 083 Logger logger, 084 Supplier<? extends Thread> threadGetter, 085 boolean logging, 086 boolean stackTraces 087 ) { 088 diagnostics = new ConcurrentLinkedQueue<>(); 089 this.logger = requireNonNull(logger, "logger"); 090 this.threadGetter = requireNonNull(threadGetter, "threadGetter"); 091 this.logging = logging; 092 this.stackTraces = stackTraces; 093 } 094 095 /** 096 * Determine if logging is enabled. 097 * 098 * @return {@code true} if enabled, or {@code false} if disabled. 099 */ 100 public boolean isLoggingEnabled() { 101 return logging; 102 } 103 104 /** 105 * Determine if stack trace reporting is enabled. 106 * 107 * <p>Note that stack traces are only reported if {@link #isLoggingEnabled() logging is enabled} 108 * as well. 109 * 110 * @return {@code true} if enabled, or {@code false} if disabled. 111 */ 112 public boolean isStackTraceReportingEnabled() { 113 return stackTraces; 114 } 115 116 /** 117 * Get a copy of the queue containing all the diagnostics that have been detected. 118 * 119 * @return the diagnostics in a list. 120 */ 121 public List<TraceDiagnostic<S>> getDiagnostics() { 122 return List.copyOf(diagnostics); 123 } 124 125 @Override 126 public final void report(Diagnostic<? extends S> diagnostic) { 127 requireNonNull(diagnostic); 128 129 var now = Instant.now(); 130 var thisThread = threadGetter.get(); 131 var threadName = thisThread.getName(); 132 var stackTrace = List.of(thisThread.getStackTrace()); 133 var threadId = LoomPolyfill.getThreadId(thisThread); 134 135 var wrapped = new TraceDiagnostic<S>(now, threadId, threadName, stackTrace, diagnostic); 136 137 diagnostics.add(wrapped); 138 139 if (!logging) { 140 return; 141 } 142 143 logger 144 .atLevel(diagnosticToLevel(diagnostic)) 145 .setMessage("{}{}") 146 .addArgument(messageGetter(wrapped)) 147 .addArgument(stackTraceFormatter(stackTrace)) 148 .log(); 149 } 150 151 private Level diagnosticToLevel(Diagnostic<?> diagnostic) { 152 switch (diagnostic.getKind()) { 153 case ERROR: 154 return Level.ERROR; 155 case WARNING: 156 case MANDATORY_WARNING: 157 return Level.WARN; 158 default: 159 return Level.INFO; 160 } 161 } 162 163 private Supplier<String> messageGetter(Diagnostic<?> diagnostic) { 164 return () -> diagnostic.getMessage(Locale.ROOT); 165 } 166 167 private Supplier<String> stackTraceFormatter(List<StackTraceElement> stackTrace) { 168 if (!stackTraces) { 169 return () -> ""; 170 } 171 172 return () -> stackTrace 173 .stream() 174 .map(frame -> "\n\t" + frame) 175 .collect(Collectors.joining()); 176 } 177}