IJPL-215943 [log] read attachments only once during saving on disk

GitOrigin-RevId: d0a9d31cf7752e5d0eae2ffb08a52ffa7a78f76b
This commit is contained in:
Max Medvedev
2025-11-11 18:38:29 +00:00
committed by intellij-monorepo-bot
parent 182d3fa53e
commit 84dc0d1ebc
@@ -16,64 +16,85 @@ import kotlin.io.path.name
/**
* Handler for logging attachments of [ExceptionWithAttachments] to log folder.
*/
internal class AttachmentHandler(private val logPath: Path) : Handler() {
internal class AttachmentHandler(private val logPath: Path ) : Handler() {
override fun publish(record: LogRecord) {
if (!isLoggable(record)) return
val t = record.thrown ?: return
val ewas = ExceptionUtil.findCauseAndSuppressed(t, ExceptionWithAttachments::class.java).ifEmpty { return }
val hasAnyAttachments = ewas.any { it.attachments.isNotEmpty() }
if (!hasAnyAttachments) return
val attachmentsDir = prepareDir(logPath, record) ?: return
log.info("Saving attachments of [${record.loggerName}] ${t.javaClass.name} to $attachmentsDir")
writeStacktrace(attachmentsDir, t)
if (ewas.singleOrNull() == t) {
// Single EWA: write files directly into the main folder (no nested folder)
writeAttachments(attachmentsDir, ewas.single().attachments)
val dirWithLoggedAttachments = if (ewas.singleOrNull() == t) {
writeSingleEwa(ewas.single(), t)
}
else {
// Multiple EWAs: create a separate subfolder for each
for ((index, ewa) in ewas.withIndex()) {
if (ewa.attachments.isEmpty()) continue
val subDir = prepareEwaDir(ewa, attachmentsDir, index) ?: continue
writeEwaStacktrace(subDir, ewa)
writeAttachments(subDir, ewa.attachments)
}
writeEwas(ewas, t)
}
if (dirWithLoggedAttachments != null) {
log.info("Saving attachments of [${record.loggerName}] ${t.javaClass.name} to $dirWithLoggedAttachments")
}
}
private fun prepareEwaDir(ewa: ExceptionWithAttachments, attachmentsDir: Path, index: Int): Path? {
val subDir = attachmentsDir.resolve("ewa-" + (index + 1) + "-" + inferErrorAbbreviation(ewa))
try {
Files.createDirectories(subDir)
return subDir
private fun writeEwas(ewas: MutableList<ExceptionWithAttachments>, t: Throwable): Path? {
val attachmentsDir = prepareDir(logPath, t) ?: return null
// store all EWAs directly in the main folder, prefixing files with the EWA index
var index = 1
for (ewa in ewas) {
val attachments = ewa.attachments.ifEmpty { continue }
writeEwa(attachmentsDir, ewa, "$index-", attachments)
index++
}
catch (_: IOException) {
if (index == 1) {
// no attachments saved => delete the empty directory
try {
Files.deleteIfExists(attachmentsDir)
}
catch (_: IOException) {}
return null
}
// Keep the overall throwable stacktrace for context
writeStacktrace(attachmentsDir.resolve("stacktrace.txt"), t)
return attachmentsDir
}
private fun writeEwaStacktrace(subDir: Path, ewa: ExceptionWithAttachments) {
private fun writeSingleEwa(ewa: ExceptionWithAttachments, t: Throwable): Path? {
val attachments = ewa.attachments.ifEmpty { return null }
val attachmentsDir = prepareDir(logPath, t) ?: return null
writeEwa(attachmentsDir, ewa, "", attachments)
return attachmentsDir
}
private fun writeEwa(
attachmentsDir: Path,
ewa: ExceptionWithAttachments,
prefix: String,
attachments: Array<Attachment>,
) {
writeIndexedEwaStacktrace(attachmentsDir, ewa, prefix)
writeAttachments(attachmentsDir, attachments, prefix)
}
private fun writeIndexedEwaStacktrace(dir: Path, ewa: ExceptionWithAttachments, prefix: String) {
if (ewa is Throwable) {
writeStacktrace(subDir, ewa)
val stacktraceFile = dir.resolve("${prefix}stacktrace.txt")
writeStacktrace(stacktraceFile, ewa)
}
else {
try {
Files.write(subDir.resolve("ewa.txt"), ewa.toString().toByteArray())
Files.write(dir.resolve("${prefix}ewa.txt"), ewa.toString().toByteArray())
}
catch (_: IOException) {
}
}
}
private fun prepareDir(logPath: Path, record: LogRecord): Path? {
private fun prepareDir(logPath: Path, t: Throwable): Path? {
val baseDir = logPath.parent.resolve("attachments")
val dirName = prepareDirName(record)
val dirName = prepareDirName(t)
val attachmentsDir = baseDir.resolve(dirName)
try {
@@ -93,15 +114,14 @@ internal class AttachmentHandler(private val logPath: Path) : Handler() {
return attachmentsDir
}
private fun prepareDirName(record: LogRecord): String {
private fun prepareDirName(t: Throwable): String {
val now = ZonedDateTime.now()
val errorAbbr = inferErrorAbbreviation(record.thrown)
val errorAbbr = inferErrorAbbreviation(t)
return "attachments-" + dateFormat.format(now) + "-" + errorAbbr
}
private fun writeStacktrace(dir: Path, t: Throwable) {
private fun writeStacktrace(stacktraceFile: Path, t: Throwable) {
try {
val stacktraceFile = dir.resolve("stacktrace.txt")
PrintWriter(Files.newBufferedWriter(stacktraceFile)).use {
t.printStackTrace(it)
}
@@ -130,11 +150,11 @@ internal class AttachmentHandler(private val logPath: Path) : Handler() {
}
}
private fun writeAttachments(dir: Path, attachments: Array<Attachment>) {
private fun writeAttachments(dir: Path, attachments: Array<Attachment>, prefix: String) {
val usedNames = HashSet<String>()
for (attachment in attachments) {
val base = sanitizeFileName(attachment.name.ifEmpty { "attachment" })
val fileName = uniqueName(base, usedNames)
val fileName = uniqueName(prefix + base, usedNames)
val file = dir.resolve(fileName)
try {
Files.write(file, attachment.bytes)
@@ -149,6 +169,7 @@ internal class AttachmentHandler(private val logPath: Path) : Handler() {
* Keep at most [maxGroups] attachment groups under [baseDir]. If the number of existing groups
* is >= [maxGroups], delete the oldest ones to make room for a new group.
*/
@Suppress("SameParameterValue")
private fun pruneOldAttachmentGroups(baseDir: Path, maxGroups: Int) {
val entries = try {
val directoryStream = Files.newDirectoryStream(baseDir) { path ->