Переглянути джерело

chore(临考突击报告): 增强 Azure 上传诊断日志

金逸霄 6 днів тому
батько
коміт
2e7ebd2f0b

+ 78 - 3
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java

@@ -2,11 +2,17 @@ package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;
 
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+import com.azure.core.exception.HttpResponseException;
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.rest.Response;
 import com.azure.core.util.Context;
 import com.azure.storage.blob.BlobClient;
 import com.azure.storage.blob.BlobContainerClient;
 import com.azure.storage.blob.BlobContainerClientBuilder;
 import com.azure.storage.blob.models.BlobHttpHeaders;
+import com.azure.storage.blob.models.BlobStorageException;
+import com.azure.storage.blob.models.BlockBlobItem;
 import com.azure.storage.blob.options.BlobParallelUploadOptions;
 import com.azure.storage.common.StorageSharedKeyCredential;
 import org.slf4j.Logger;
@@ -92,16 +98,27 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
                 .setHeaders(new BlobHttpHeaders().setContentType("application/pdf"))
                 .setMetadata(metadata);
         long uploadRequestStartedNanos = System.nanoTime();
-        blobClient.uploadWithResponse(options, Context.NONE);
+        Response<BlockBlobItem> uploadResponse;
+        try {
+            uploadResponse = blobClient.uploadWithResponse(options, Context.NONE);
+        } catch (RuntimeException e) {
+            logUploadFailure(reportId, reportType, fileName, pdfBytes.length, blobName, uploadRequestStartedNanos, e);
+            throw e;
+        }
         log.info(
-                "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=upload_request_completed storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
+                "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=upload_request_completed storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureETag={} azureLastModified={}",
                 reportId,
                 reportType,
                 blobName,
                 blobName,
                 fileName,
                 pdfBytes.length,
-                elapsedMillis(uploadRequestStartedNanos));
+                elapsedMillis(uploadRequestStartedNanos),
+                uploadResponse.getStatusCode(),
+                headerValue(uploadResponse.getHeaders(), HttpHeaderName.X_MS_REQUEST_ID),
+                headerValue(uploadResponse.getHeaders(), HttpHeaderName.X_MS_CLIENT_REQUEST_ID),
+                uploadResponse.getValue() == null ? null : uploadResponse.getValue().getETag(),
+                uploadResponse.getValue() == null ? null : uploadResponse.getValue().getLastModified());
         log.info(
                 "exam_sprint_report_azure_storage_upload_completed reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
                 reportId,
@@ -220,6 +237,64 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
         return builder.buildClient();
     }
 
+    private void logUploadFailure(
+            String reportId,
+            ReportType reportType,
+            String fileName,
+            int pdfByteLength,
+            String blobName,
+            long uploadRequestStartedNanos,
+            RuntimeException exception) {
+        HttpHeaders headers = responseHeaders(exception);
+        log.warn(
+                "exam_sprint_report_azure_storage_upload_failed reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} exceptionType={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureErrorCode={}",
+                reportId,
+                reportType,
+                blobName,
+                blobName,
+                fileName,
+                pdfByteLength,
+                elapsedMillis(uploadRequestStartedNanos),
+                exception.getClass().getSimpleName(),
+                azureStatusCode(exception),
+                headerValue(headers, HttpHeaderName.X_MS_REQUEST_ID),
+                headerValue(headers, HttpHeaderName.X_MS_CLIENT_REQUEST_ID),
+                azureErrorCode(exception));
+    }
+
+    private static Integer azureStatusCode(RuntimeException exception) {
+        if (exception instanceof BlobStorageException blobStorageException) {
+            return blobStorageException.getStatusCode();
+        }
+        if (exception instanceof HttpResponseException httpResponseException && httpResponseException.getResponse() != null) {
+            return httpResponseException.getResponse().getStatusCode();
+        }
+        return null;
+    }
+
+    private static String azureErrorCode(RuntimeException exception) {
+        if (exception instanceof BlobStorageException blobStorageException && blobStorageException.getErrorCode() != null) {
+            return blobStorageException.getErrorCode().toString();
+        }
+        HttpHeaders headers = responseHeaders(exception);
+        return headerValue(headers, "x-ms-error-code");
+    }
+
+    private static HttpHeaders responseHeaders(RuntimeException exception) {
+        if (exception instanceof HttpResponseException httpResponseException && httpResponseException.getResponse() != null) {
+            return httpResponseException.getResponse().getHeaders();
+        }
+        return null;
+    }
+
+    private static String headerValue(HttpHeaders headers, HttpHeaderName headerName) {
+        return headers == null ? null : headers.getValue(headerName);
+    }
+
+    private static String headerValue(HttpHeaders headers, String headerName) {
+        return headers == null ? null : headers.getValue(headerName);
+    }
+
     private static boolean hasText(String value) {
         return value != null && !value.isBlank();
     }

+ 87 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java

@@ -2,9 +2,17 @@ package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;
 
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.http.HttpResponse;
+import com.azure.core.http.rest.SimpleResponse;
 import com.azure.core.util.Context;
 import com.azure.storage.blob.BlobClient;
 import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.models.BlobStorageException;
+import com.azure.storage.blob.models.BlockBlobItem;
 import com.azure.storage.blob.options.BlobParallelUploadOptions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -14,9 +22,11 @@ import org.springframework.boot.test.system.OutputCaptureExtension;
 
 import java.lang.reflect.Constructor;
 import java.net.URI;
+import java.net.URL;
 import java.time.Clock;
 import java.time.Duration;
 import java.time.Instant;
+import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -202,6 +212,8 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
+        when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
+                .thenReturn(successfulUploadResponse());
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 "report",
@@ -227,6 +239,51 @@ class AzureBlobExamSprintReportStorageTest {
                 .contains("fileName=file.pdf")
                 .contains("pdfByteLength=3")
                 .contains("durationMs=")
+                .contains("azureStatusCode=201")
+                .contains("azureRequestId=azure-request-123")
+                .contains("azureClientRequestId=client-request-456")
+                .contains("azureETag=etag-789")
+                .contains("azureLastModified=2026-01-03T08:01Z")
+                .doesNotContain("connection-string")
+                .doesNotContain("accountKey")
+                .doesNotContain("account-key");
+    }
+
+    @Test
+    void uploadLogsAzureDiagnosticsAndRethrowsWhenUploadRequestFails(CapturedOutput output) throws Exception {
+        BlobContainerClient containerClient = mock(BlobContainerClient.class);
+        BlobClient blobClient = mock(BlobClient.class);
+        when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
+        when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
+                .thenThrow(blobStorageException());
+        AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
+                containerClient,
+                "report",
+                "https://dcjxbtest.blob.core.chinacloudapi.cn",
+                FIXED_CLOCK);
+
+        assertThatThrownBy(() -> storage.upload(
+                "report-123",
+                ReportType.OUTLOOK,
+                "file.pdf",
+                new byte[]{1, 2, 3},
+                Instant.parse("2026-01-10T00:00:00Z")))
+                .isInstanceOf(BlobStorageException.class);
+
+        assertThat(output.getAll())
+                .contains("exam_sprint_report_azure_storage_upload_failed")
+                .contains("reportId=report-123")
+                .contains("reportType=OUTLOOK")
+                .contains("storageObjectKey=reports/outlook/report-123/file.pdf")
+                .contains("blobName=reports/outlook/report-123/file.pdf")
+                .contains("fileName=file.pdf")
+                .contains("pdfByteLength=3")
+                .contains("durationMs=")
+                .contains("exceptionType=BlobStorageException")
+                .contains("azureStatusCode=503")
+                .contains("azureRequestId=azure-request-failed")
+                .contains("azureClientRequestId=client-request-failed")
+                .contains("azureErrorCode=ServerBusy")
                 .doesNotContain("connection-string")
                 .doesNotContain("accountKey")
                 .doesNotContain("account-key");
@@ -262,4 +319,34 @@ class AzureBlobExamSprintReportStorageTest {
         assertThat(firstStoredFile.fileName()).isEqualTo("same-display-name.pdf");
         assertThat(secondStoredFile.fileName()).isEqualTo("same-display-name.pdf");
     }
+
+    private SimpleResponse<BlockBlobItem> successfulUploadResponse() {
+        HttpHeaders headers = new HttpHeaders()
+                .set(HttpHeaderName.X_MS_REQUEST_ID, "azure-request-123")
+                .set(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-456");
+        BlockBlobItem blockBlobItem = new BlockBlobItem(
+                "etag-789",
+                OffsetDateTime.parse("2026-01-03T08:01:00Z"),
+                null,
+                true,
+                null);
+        return new SimpleResponse<>(
+                new HttpRequest(HttpMethod.PUT, "https://example.test/report/file.pdf"),
+                201,
+                headers,
+                blockBlobItem);
+    }
+
+    private BlobStorageException blobStorageException() throws Exception {
+        HttpResponse response = mock(HttpResponse.class);
+        when(response.getStatusCode()).thenReturn(503);
+        when(response.getHeaders()).thenReturn(new HttpHeaders()
+                .set(HttpHeaderName.X_MS_REQUEST_ID, "azure-request-failed")
+                .set(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-failed")
+                .set("x-ms-error-code", "ServerBusy"));
+        when(response.getHeaderValue(HttpHeaderName.X_MS_REQUEST_ID)).thenReturn("azure-request-failed");
+        when(response.getHeaderValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID)).thenReturn("client-request-failed");
+        when(response.getRequest()).thenReturn(new HttpRequest(HttpMethod.PUT, new URL("https://example.test/report/file.pdf")));
+        return new BlobStorageException("ServerBusy", response, null);
+    }
 }