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