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,7 +1,9 @@
import {
copyApiRequestId,
ensureAuthentication,
getApiBaseUrl,
getApiErrorMessage,
getApiRequestId,
getHealth,
setApiBaseUrl
} from '../../utils/api'
@@ -13,10 +15,12 @@ Page({
apiBaseUrl: getApiBaseUrl(),
connectionText: '尚未检测',
connectionTone: 'idle',
connectionRequestId: '',
testing: false,
authText: '尚未登录',
authTone: 'idle',
authUserId: ''
authUserId: '',
authRequestId: ''
},
onShow() {
@@ -37,12 +41,21 @@ Page({
await this.checkConnection()
await this.checkAuthentication()
} catch (error) {
this.setData({ connectionText: getApiErrorMessage(error), connectionTone: 'error' })
this.setData({
connectionText: getApiErrorMessage(error),
connectionTone: 'error',
connectionRequestId: getApiRequestId(error)
})
}
},
async checkConnection() {
this.setData({ testing: true, connectionText: '正在检测服务...', connectionTone: 'idle' })
this.setData({
testing: true,
connectionText: '正在检测服务...',
connectionTone: 'idle',
connectionRequestId: ''
})
try {
const health = await getHealth()
let connectionText = '服务、数据库和语音转录正常'
@@ -62,30 +75,42 @@ Page({
}
this.setData({
connectionText,
connectionTone
connectionTone,
connectionRequestId: ''
})
} catch (error) {
this.setData({ connectionText: getApiErrorMessage(error), connectionTone: 'error' })
this.setData({
connectionText: getApiErrorMessage(error),
connectionTone: 'error',
connectionRequestId: getApiRequestId(error)
})
} finally {
this.setData({ testing: false })
}
},
async checkAuthentication() {
this.setData({ authText: '正在登录...', authTone: 'idle', authUserId: '' })
this.setData({ authText: '正在登录...', authTone: 'idle', authUserId: '', authRequestId: '' })
try {
const session = await ensureAuthentication()
this.setData({
authText: '微信身份已登录',
authTone: 'success',
authUserId: session.userId
authUserId: session.userId,
authRequestId: ''
})
} catch (error) {
this.setData({
authText: getApiErrorMessage(error),
authTone: 'error',
authUserId: ''
authUserId: '',
authRequestId: getApiRequestId(error)
})
}
},
copyRequestId(event: WechatMiniprogram.TouchEvent) {
const { requestId } = event.currentTarget.dataset as { requestId: string }
copyApiRequestId(requestId)
}
})

View File

@@ -13,6 +13,10 @@
<text class="connection-dot connection-{{connectionTone}}"></text>
<text class="connection-text">{{connectionText}}</text>
</view>
<view wx:if="{{connectionRequestId}}" class="request-id-row">
<text>请求编号:{{connectionRequestId}}</text>
<text class="status-action" data-request-id="{{connectionRequestId}}" bindtap="copyRequestId">复制</text>
</view>
<button class="primary-button check-button" loading="{{testing}}" disabled="{{testing}}" bindtap="saveAndCheck">保存并检测</button>
</view>
@@ -22,6 +26,10 @@
<text class="connection-dot connection-{{authTone}}"></text>
<text class="connection-text">{{authText}}</text>
</view>
<view wx:if="{{authRequestId}}" class="request-id-row">
<text>请求编号:{{authRequestId}}</text>
<text class="status-action" data-request-id="{{authRequestId}}" bindtap="copyRequestId">复制</text>
</view>
<text wx:if="{{authUserId}}" class="identity-value">{{authUserId}}</text>
</view>
</view>

View File

@@ -70,6 +70,10 @@
font-size: 25rpx;
}
.request-id-row {
margin-left: 28rpx;
}
.check-button {
margin-top: 24rpx;
}