mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat(data-sync): implement logging for data synchronization process
- Added logging functionality to the DataSyncService to track progress and errors during the manifest generation process. - Introduced DataSyncLogLevel and DataSyncLogPayload types for structured logging. - Updated emitLog method to handle different log levels (info, success, warn, error) and include relevant details. - Enhanced PhotoSyncProgressState to maintain a log of synchronization events, displayed in the PhotoSyncProgressPanel. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import type {
|
||||
ConflictPayload,
|
||||
DataSyncAction,
|
||||
DataSyncConflict,
|
||||
DataSyncLogLevel,
|
||||
DataSyncOptions,
|
||||
DataSyncProgressEmitter,
|
||||
DataSyncProgressStage,
|
||||
@@ -368,6 +369,10 @@ export class DataSyncService {
|
||||
|
||||
const result = await this.safeProcessStorageObject(storageObject, builder, {
|
||||
livePhotoMap,
|
||||
progress: {
|
||||
emitter: onProgress,
|
||||
stage: 'missing-in-db',
|
||||
},
|
||||
})
|
||||
|
||||
if (!result?.item) {
|
||||
@@ -944,6 +949,33 @@ export class DataSyncService {
|
||||
})
|
||||
}
|
||||
|
||||
private async emitLog(
|
||||
emitter: DataSyncProgressEmitter | undefined,
|
||||
payload: {
|
||||
level: DataSyncLogLevel
|
||||
message: string
|
||||
stage?: DataSyncProgressStage | null
|
||||
storageKey?: string
|
||||
details?: Record<string, unknown> | null
|
||||
},
|
||||
): Promise<void> {
|
||||
if (!emitter) {
|
||||
return
|
||||
}
|
||||
|
||||
await emitter({
|
||||
type: 'log',
|
||||
payload: {
|
||||
level: payload.level,
|
||||
message: payload.message,
|
||||
stage: payload.stage ?? null,
|
||||
storageKey: payload.storageKey,
|
||||
details: payload.details ?? null,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async emitComplete(emitter: DataSyncProgressEmitter | undefined, result: DataSyncResult): Promise<void> {
|
||||
if (!emitter) {
|
||||
return
|
||||
@@ -1035,10 +1067,29 @@ export class DataSyncService {
|
||||
options: {
|
||||
existing?: PhotoManifestItem | null
|
||||
livePhotoMap?: Map<string, StorageObject>
|
||||
progress?: {
|
||||
emitter?: DataSyncProgressEmitter
|
||||
stage?: DataSyncProgressStage | null
|
||||
}
|
||||
},
|
||||
) {
|
||||
const progressContext = options.progress
|
||||
const stage = progressContext?.stage ?? null
|
||||
const emitter = progressContext?.emitter
|
||||
|
||||
await this.emitLog(emitter, {
|
||||
level: 'info',
|
||||
message: '开始生成 manifest',
|
||||
stage,
|
||||
storageKey: storageObject.key,
|
||||
details: {
|
||||
hasExistingManifest: Boolean(options.existing),
|
||||
hasLivePhotoMap: Boolean(options.livePhotoMap),
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
return await this.photoBuilderService.processPhotoFromStorageObject(storageObject, {
|
||||
const result = await this.photoBuilderService.processPhotoFromStorageObject(storageObject, {
|
||||
existingItem: options.existing ?? undefined,
|
||||
livePhotoMap: options.livePhotoMap,
|
||||
processorOptions: {
|
||||
@@ -1047,7 +1098,42 @@ export class DataSyncService {
|
||||
},
|
||||
builder,
|
||||
})
|
||||
|
||||
if (result?.item) {
|
||||
await this.emitLog(emitter, {
|
||||
level: 'success',
|
||||
message: '生成 manifest 成功',
|
||||
stage,
|
||||
storageKey: storageObject.key,
|
||||
details: {
|
||||
photoId: result.item.id,
|
||||
resultType: result.type ?? null,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await this.emitLog(emitter, {
|
||||
level: 'warn',
|
||||
message: '生成 manifest 未返回照片数据',
|
||||
stage,
|
||||
storageKey: storageObject.key,
|
||||
details: {
|
||||
resultType: result?.type ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
await this.emitLog(emitter, {
|
||||
level: 'error',
|
||||
message: '生成 manifest 失败',
|
||||
stage,
|
||||
storageKey: storageObject.key,
|
||||
details: {
|
||||
error: message,
|
||||
},
|
||||
})
|
||||
this.logger.error('Failed to process storage object', err)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -89,6 +89,17 @@ export interface DataSyncStageTotals {
|
||||
'status-reconciliation': number
|
||||
}
|
||||
|
||||
export type DataSyncLogLevel = 'info' | 'success' | 'warn' | 'error'
|
||||
|
||||
export interface DataSyncLogPayload {
|
||||
level: DataSyncLogLevel
|
||||
message: string
|
||||
timestamp: string
|
||||
stage?: DataSyncProgressStage | null
|
||||
storageKey?: string
|
||||
details?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export type DataSyncProgressEvent =
|
||||
| {
|
||||
type: 'start'
|
||||
@@ -128,5 +139,9 @@ export type DataSyncProgressEvent =
|
||||
message: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'log'
|
||||
payload: DataSyncLogPayload
|
||||
}
|
||||
|
||||
export type DataSyncProgressEmitter = (event: DataSyncProgressEvent) => Promise<void> | void
|
||||
|
||||
@@ -42,6 +42,8 @@ const STAGE_ORDER: PhotoSyncProgressStage[] = [
|
||||
'status-reconciliation',
|
||||
]
|
||||
|
||||
const MAX_SYNC_LOGS = 200
|
||||
|
||||
function createInitialStages(totals: PhotoSyncProgressState['totals']): PhotoSyncProgressState['stages'] {
|
||||
return STAGE_ORDER.reduce<PhotoSyncProgressState['stages']>(
|
||||
(acc, stage) => {
|
||||
@@ -109,6 +111,7 @@ export function PhotoPage() {
|
||||
stages: createInitialStages(totals),
|
||||
startedAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
logs: [],
|
||||
lastAction: undefined,
|
||||
error: undefined,
|
||||
})
|
||||
@@ -135,6 +138,34 @@ export function PhotoPage() {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'log') {
|
||||
setSyncProgress((prev) => {
|
||||
if (!prev) {
|
||||
return prev
|
||||
}
|
||||
|
||||
const parsedTimestamp = Date.parse(event.payload.timestamp)
|
||||
const entry = {
|
||||
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
timestamp: Number.isNaN(parsedTimestamp) ? Date.now() : parsedTimestamp,
|
||||
level: event.payload.level,
|
||||
message: event.payload.message,
|
||||
stage: event.payload.stage ?? null,
|
||||
storageKey: event.payload.storageKey ?? undefined,
|
||||
details: event.payload.details ?? null,
|
||||
}
|
||||
|
||||
const nextLogs = prev.logs.length >= MAX_SYNC_LOGS ? [...prev.logs.slice(1), entry] : [...prev.logs, entry]
|
||||
|
||||
return {
|
||||
...prev,
|
||||
logs: nextLogs,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'stage') {
|
||||
setSyncProgress((prev) => {
|
||||
if (!prev) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Spring } from '@afilmory/utils'
|
||||
import { m } from 'motion/react'
|
||||
|
||||
import { getConflictTypeLabel, PHOTO_ACTION_TYPE_CONFIG } from '../../constants'
|
||||
import type { PhotoSyncAction, PhotoSyncProgressStage, PhotoSyncProgressState } from '../../types'
|
||||
import type { PhotoSyncAction, PhotoSyncLogLevel, PhotoSyncProgressStage, PhotoSyncProgressState } from '../../types'
|
||||
import { BorderOverlay } from './PhotoSyncResultPanel'
|
||||
|
||||
const STAGE_CONFIG: Record<PhotoSyncProgressStage, { label: string; description: string }> = {
|
||||
@@ -37,6 +37,13 @@ const STATUS_LABEL: Record<PhotoSyncProgressState['stages'][PhotoSyncProgressSta
|
||||
completed: '已完成',
|
||||
}
|
||||
|
||||
const LOG_LEVEL_CONFIG: Record<PhotoSyncLogLevel, { label: string; className: string }> = {
|
||||
info: { label: '信息', className: 'border border-sky-500/30 bg-sky-500/10 text-sky-200' },
|
||||
success: { label: '成功', className: 'border border-emerald-500/30 bg-emerald-500/10 text-emerald-200' },
|
||||
warn: { label: '警告', className: 'border border-amber-500/30 bg-amber-500/10 text-amber-200' },
|
||||
error: { label: '错误', className: 'border border-rose-500/30 bg-rose-500/10 text-rose-200' },
|
||||
}
|
||||
|
||||
const SUMMARY_FIELDS: Array<{
|
||||
key: keyof PhotoSyncProgressState['summary']
|
||||
label: string
|
||||
@@ -46,6 +53,20 @@ const SUMMARY_FIELDS: Array<{
|
||||
{ key: 'conflicts', label: '冲突' },
|
||||
]
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
|
||||
function formatLogTimestamp(timestamp: number): string {
|
||||
try {
|
||||
return timeFormatter.format(timestamp)
|
||||
} catch {
|
||||
return '--:--:--'
|
||||
}
|
||||
}
|
||||
|
||||
type PhotoSyncProgressPanelProps = {
|
||||
progress: PhotoSyncProgressState
|
||||
}
|
||||
@@ -94,6 +115,7 @@ export function PhotoSyncProgressPanel({ progress }: PhotoSyncProgressPanelProps
|
||||
}))
|
||||
|
||||
const { lastAction } = progress
|
||||
const recentLogs = progress.logs.slice(-8).reverse()
|
||||
|
||||
return (
|
||||
<div className="bg-background-tertiary relative overflow-hidden rounded-lg p-6">
|
||||
@@ -147,6 +169,63 @@ export function PhotoSyncProgressPanel({ progress }: PhotoSyncProgressPanelProps
|
||||
))}
|
||||
</div>
|
||||
|
||||
{recentLogs.length > 0 ? (
|
||||
<div className="border-border/20 bg-fill/10 mt-6 overflow-hidden rounded-lg border p-4">
|
||||
<BorderOverlay />
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-text text-sm font-semibold">构建日志</p>
|
||||
<span className="text-text-tertiary text-xs">最新 {recentLogs.length} 条</span>
|
||||
</div>
|
||||
<div className="mt-3 max-h-48 space-y-2 overflow-y-auto pr-1">
|
||||
{recentLogs.map((log) => {
|
||||
const levelConfig = LOG_LEVEL_CONFIG[log.level]
|
||||
const details = (log.details ?? null) as Record<string, unknown> | null
|
||||
const photoId = details && typeof details['photoId'] === 'string' ? (details['photoId'] as string) : null
|
||||
const resultType =
|
||||
details && typeof details['resultType'] === 'string' ? (details['resultType'] as string) : null
|
||||
const error = details && typeof details['error'] === 'string' ? (details['error'] as string) : null
|
||||
const hasExisting =
|
||||
details && typeof details['hasExistingManifest'] === 'boolean'
|
||||
? (details['hasExistingManifest'] as boolean)
|
||||
: null
|
||||
const hasLivePhotoMap =
|
||||
details && typeof details['hasLivePhotoMap'] === 'boolean'
|
||||
? (details['hasLivePhotoMap'] as boolean)
|
||||
: null
|
||||
|
||||
const detailSegments: string[] = []
|
||||
if (photoId) detailSegments.push(`ID ${photoId}`)
|
||||
if (resultType) detailSegments.push(`结果 ${resultType}`)
|
||||
if (typeof hasExisting === 'boolean') {
|
||||
detailSegments.push(hasExisting ? '包含历史 manifest' : '无历史 manifest')
|
||||
}
|
||||
if (typeof hasLivePhotoMap === 'boolean') {
|
||||
detailSegments.push(hasLivePhotoMap ? '包含 Live Photo' : '无 Live Photo')
|
||||
}
|
||||
if (error) detailSegments.push(`错误 ${error}`)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={log.id}
|
||||
className="bg-background-secondary/40 text-text flex flex-wrap items-center gap-2 rounded-md px-3 py-2 text-xs"
|
||||
>
|
||||
<span className="text-text-tertiary tabular-nums">{formatLogTimestamp(log.timestamp)}</span>
|
||||
<span className={`${levelConfig.className} rounded-full px-2 py-0.5 text-[11px] font-medium`}>
|
||||
{levelConfig.label}
|
||||
</span>
|
||||
<span className="text-text">{log.message}</span>
|
||||
{log.storageKey ? <code className="text-text-secondary">{log.storageKey}</code> : null}
|
||||
{log.stage ? <span className="text-text-tertiary">{STAGE_CONFIG[log.stage].label}</span> : null}
|
||||
{detailSegments.length > 0 ? (
|
||||
<span className="text-text-tertiary">{detailSegments.join(' · ')}</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{lastAction ? (
|
||||
<div className="border-border/20 bg-fill/10 mt-6 overflow-hidden rounded-lg border p-4">
|
||||
<BorderOverlay />
|
||||
|
||||
@@ -78,6 +78,17 @@ export interface PhotoSyncStageTotals {
|
||||
'status-reconciliation': number
|
||||
}
|
||||
|
||||
export type PhotoSyncLogLevel = 'info' | 'success' | 'warn' | 'error'
|
||||
|
||||
export interface PhotoSyncLogPayload {
|
||||
level: PhotoSyncLogLevel
|
||||
message: string
|
||||
timestamp: string
|
||||
stage?: PhotoSyncProgressStage | null
|
||||
storageKey?: string
|
||||
details?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export type PhotoSyncProgressEvent =
|
||||
| {
|
||||
type: 'start'
|
||||
@@ -117,6 +128,10 @@ export type PhotoSyncProgressEvent =
|
||||
message: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'log'
|
||||
payload: PhotoSyncLogPayload
|
||||
}
|
||||
|
||||
export type PhotoSyncStageStatus = 'pending' | 'running' | 'completed'
|
||||
|
||||
@@ -134,6 +149,7 @@ export interface PhotoSyncProgressState {
|
||||
>
|
||||
startedAt: number
|
||||
updatedAt: number
|
||||
logs: PhotoSyncLogEntry[]
|
||||
lastAction?: {
|
||||
stage: PhotoSyncProgressStage
|
||||
index: number
|
||||
@@ -143,6 +159,16 @@ export interface PhotoSyncProgressState {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface PhotoSyncLogEntry {
|
||||
id: string
|
||||
timestamp: number
|
||||
level: PhotoSyncLogLevel
|
||||
message: string
|
||||
stage?: PhotoSyncProgressStage | null
|
||||
storageKey?: string
|
||||
details?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface PhotoAssetManifestPayload {
|
||||
version: string
|
||||
data: PhotoManifestItem
|
||||
|
||||
@@ -14,6 +14,9 @@ import type { PickedExif } from '../types/photo.js'
|
||||
export async function extractExifData(imageBuffer: Buffer, originalBuffer?: Buffer): Promise<PickedExif | null> {
|
||||
const log = getGlobalLoggers().exif
|
||||
|
||||
await mkdir('/tmp/image_process', { recursive: true })
|
||||
const tempImagePath = path.resolve('/tmp/image_process', `${crypto.randomUUID()}.jpg`)
|
||||
|
||||
try {
|
||||
log.info('开始提取 EXIF 数据')
|
||||
|
||||
@@ -35,15 +38,10 @@ export async function extractExifData(imageBuffer: Buffer, originalBuffer?: Buff
|
||||
return null
|
||||
}
|
||||
|
||||
await mkdir('/tmp/image_process', { recursive: true })
|
||||
const tempImagePath = path.resolve('/tmp/image_process', `${crypto.randomUUID()}.jpg`)
|
||||
|
||||
await writeFile(tempImagePath, originalBuffer || imageBuffer)
|
||||
const exifData = await exiftool.read(tempImagePath)
|
||||
const result = handleExifData(exifData, metadata)
|
||||
|
||||
await unlink(tempImagePath).catch(noop)
|
||||
|
||||
if (!exifData) {
|
||||
log.warn('EXIF 数据解析失败')
|
||||
return null
|
||||
@@ -59,6 +57,8 @@ export async function extractExifData(imageBuffer: Buffer, originalBuffer?: Buff
|
||||
} catch (error) {
|
||||
log.error('提取 EXIF 数据失败:', error)
|
||||
return null
|
||||
} finally {
|
||||
await unlink(tempImagePath).catch(noop)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user