Observability: Prometheus metrics, log panel, runbook
- GET /api/metrics renders Prometheus text (equity, P&L, per-strategy equity/pnl/allocation/halted, info) with no client library. - In-memory ring log handler (app/logs.py) exposed at GET /api/logs and a dashboard "Server logs" panel; Runtime mirrors bot events into the logger. - docs/runbook.md covers deploys, the kill switch, resetting paper state and going live. Adds tests for metrics rendering and the log ring.
This commit is contained in:
1 parent
c605a0f316
commit
ac92dd5ac4
12 files changed
+462
-1
No files matched your search
@@ -4,6 +4,7 @@ import * as api from './api'
|
||||
import BacktestPanel from './components/BacktestPanel.vue'
|
||||
import EquityChart from './components/EquityChart.vue'
|
||||
import EventLog from './components/EventLog.vue'
|
||||
import LogsPanel from './components/LogsPanel.vue'
|
||||
import MultiStrategyPanel from './components/MultiStrategyPanel.vue'
|
||||
import OrdersTable from './components/OrdersTable.vue'
|
||||
import PositionsTable from './components/PositionsTable.vue'
|
||||
@@ -374,6 +375,8 @@ onUnmounted(() => {
|
||||
<EventLog :events="events" />
|
||||
</div>
|
||||
|
||||
<LogsPanel />
|
||||
|
||||
<PositionsTable :positions="positions" @close="handleClose" />
|
||||
<OrdersTable :orders="orders" />
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ export const getPositions = () => http.get('/positions').then((r) => r.data)
|
||||
export const getOrders = () => http.get('/orders').then((r) => r.data)
|
||||
export const getEvents = () => http.get('/events').then((r) => r.data)
|
||||
export const getEquity = () => http.get('/equity').then((r) => r.data)
|
||||
export const getLogs = (limit = 100) =>
|
||||
http.get('/logs', { params: { limit } }).then((r) => r.data)
|
||||
|
||||
export const startBot = (payload) => http.post('/bot/start', payload).then((r) => r.data)
|
||||
export const stopBot = () => http.post('/bot/stop').then((r) => r.data)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import * as api from '../api'
|
||||
import { fmtTime } from '../format'
|
||||
import CollapsiblePanel from './CollapsiblePanel.vue'
|
||||
|
||||
const entries = ref([])
|
||||
const levelFilter = ref('all')
|
||||
let timer
|
||||
|
||||
const levelClass = {
|
||||
info: 'text-slate-300',
|
||||
warning: 'text-amber-300',
|
||||
error: 'text-rose-300',
|
||||
critical: 'text-rose-400',
|
||||
debug: 'text-slate-500',
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (levelFilter.value === 'all') return entries.value
|
||||
return entries.value.filter((e) => e.level === levelFilter.value)
|
||||
})
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const data = await api.getLogs(200)
|
||||
entries.value = data.entries || []
|
||||
} catch {
|
||||
// ignore transient errors
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
timer = setInterval(load, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsiblePanel
|
||||
title="Server logs"
|
||||
storage-key="logs"
|
||||
:default-open="false"
|
||||
:summary="`${entries.length} lines`"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="space-y-1">
|
||||
<span class="label">Level</span>
|
||||
<select v-model="levelFilter" class="input">
|
||||
<option value="all">all</option>
|
||||
<option value="info">info</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="error">error</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="max-h-72 space-y-0.5 overflow-y-auto font-mono text-xs">
|
||||
<div v-for="(e, i) in filtered" :key="i" class="flex gap-2 border-b border-edge/40 py-0.5">
|
||||
<span class="shrink-0 text-slate-500">{{ fmtTime(e.time) }}</span>
|
||||
<span class="shrink-0" :class="levelClass[e.level] || 'text-slate-300'">
|
||||
[{{ e.level }}]
|
||||
</span>
|
||||
<span class="shrink-0 text-slate-500">{{ e.logger }}</span>
|
||||
<span :class="levelClass[e.level] || 'text-slate-300'">{{ e.message }}</span>
|
||||
</div>
|
||||
<div v-if="!filtered.length" class="py-6 text-center text-slate-500">No log lines</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsiblePanel>
|
||||
</template>
|
||||
Reference in new issue
Block a user