feat(observability): add structured application logging

Emit correlated JSON logs from the API and FunASR services, propagate request IDs to the client, and configure bounded opt-in Docker logging.

Document the host-wide Grafana/Loki query workflow and keep observability deployment ownership outside the application repository.
This commit is contained in:
2026-07-22 13:52:40 +08:00
parent a63e9a1705
commit a875fe9f63
30 changed files with 1261 additions and 817 deletions

View File

@@ -1,6 +1,8 @@
import {
copyApiRequestId,
deleteFeedbackRecord,
getApiErrorMessage,
getApiRequestId,
listFeedbackRecords,
listProfiles
} from '../../utils/api'
@@ -17,7 +19,8 @@ Page({
selectedProfileIndex: 0,
loading: false,
deletingId: '',
errorMessage: ''
errorMessage: '',
errorRequestId: ''
},
onShow() {
@@ -26,7 +29,7 @@ Page({
},
async loadData() {
this.setData({ loading: true, errorMessage: '' })
this.setData({ loading: true, errorMessage: '', errorRequestId: '' })
try {
const profiles = await listProfiles()
const selectedProfile = profiles[this.data.selectedProfileIndex - 1]
@@ -38,7 +41,7 @@ Page({
records: this.withProfileNames(records, profiles)
})
} catch (error) {
this.setData({ errorMessage: getApiErrorMessage(error) })
this.setData({ errorMessage: getApiErrorMessage(error), errorRequestId: getApiRequestId(error) })
} finally {
this.setData({ loading: false })
}
@@ -55,15 +58,19 @@ Page({
void this.loadData()
},
copyRequestId() {
copyApiRequestId(this.data.errorRequestId)
},
async onProfileChange(event: PickerEvent) {
const selectedProfileIndex = Number(event.detail.value)
const profile = this.data.profiles[selectedProfileIndex - 1]
this.setData({ selectedProfileIndex, loading: true, errorMessage: '' })
this.setData({ selectedProfileIndex, loading: true, errorMessage: '', errorRequestId: '' })
try {
const records = await listFeedbackRecords(profile?.id)
this.setData({ records: this.withProfileNames(records, this.data.profiles) })
} catch (error) {
this.setData({ errorMessage: getApiErrorMessage(error) })
this.setData({ errorMessage: getApiErrorMessage(error), errorRequestId: getApiRequestId(error) })
} finally {
this.setData({ loading: false })
}

View File

@@ -6,7 +6,13 @@
<text class="page-subtitle">按学生查阅已保存的课堂反馈。</text>
<view wx:if="{{errorMessage}}" class="status-banner">
<text>{{errorMessage}}</text>
<view class="status-copy">
<text>{{errorMessage}}</text>
<view wx:if="{{errorRequestId}}" class="status-request-id">
<text>请求编号:{{errorRequestId}}</text>
<text class="status-action" bindtap="copyRequestId">复制</text>
</view>
</view>
<text class="status-action" bindtap="retryLoad">重试</text>
</view>