SensitiveLlmPromptTurboFilter.java

package com.taxonomy.shared.logging;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.springframework.stereotype.Component;

/**
 * Prevents rendered LLM prompts from entering application logs.
 *
 * <p>Rendered prompts can contain requirement text, document excerpts and other
 * project data. The filter is deliberately independent of the configured log
 * level so enabling DEBUG for diagnostics cannot expose those values. Structured
 * request metadata and provider diagnostics remain available.</p>
 */
@Component
public final class SensitiveLlmPromptTurboFilter extends TurboFilter {

    static final String LLM_SERVICE_LOGGER = "com.taxonomy.analysis.service.LlmService";
    static final String FULL_PROMPT_MESSAGE_PREFIX = "Full LLM prompt:";

    private LoggerContext loggerContext;

    @PostConstruct
    void install() {
        if (LoggerFactory.getILoggerFactory() instanceof LoggerContext context) {
            loggerContext = context;
            start();
            context.addTurboFilter(this);
        }
    }

    @PreDestroy
    void uninstall() {
        if (loggerContext != null) {
            loggerContext.getTurboFilterList().remove(this);
            stop();
            loggerContext = null;
        }
    }

    @Override
    public FilterReply decide(Marker marker,
                              Logger logger,
                              Level level,
                              String format,
                              Object[] parameters,
                              Throwable throwable) {
        if (logger != null
                && LLM_SERVICE_LOGGER.equals(logger.getName())
                && format != null
                && format.startsWith(FULL_PROMPT_MESSAGE_PREFIX)) {
            return FilterReply.DENY;
        }
        return FilterReply.NEUTRAL;
    }
}