功能:实现 webapp 基于索引的日志导航
主要改进: - 添加索引导航支持:前进/后退按钮现在基于记录索引加载数据 - 后端 API 支持可选的 index 参数,返回包含 centerIndex 的响应 - 前端追踪 currentCenterIndex,实现精确的页面跳转 - 在状态徽章中显示记录索引号 [#索引] - 修复日志显示逻辑:从追加模式改为完全重新渲染 代码优化: - concurrent: 使用 Go 1.25 range 语法和 min 函数 - concurrent: 使用 WaitGroup.Go 方法简化 goroutine 启动 - topic_processor: 修正格式化输出 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -114,12 +114,12 @@ func main() {
|
|||||||
var totalQueries atomic.Int64
|
var totalQueries atomic.Int64
|
||||||
var queryErrors atomic.Int64
|
var queryErrors atomic.Int64
|
||||||
|
|
||||||
for i := 0; i < queryCount; i++ {
|
for i := range queryCount {
|
||||||
queryWg.Add(1)
|
queryWg.Add(1)
|
||||||
go func(queryID int) {
|
go func(queryID int) {
|
||||||
defer queryWg.Done()
|
defer queryWg.Done()
|
||||||
|
|
||||||
for j := 0; j < queriesPerGoroutine; j++ {
|
for j := range queriesPerGoroutine {
|
||||||
// 随机选择一个 topic 进行查询
|
// 随机选择一个 topic 进行查询
|
||||||
topic := topics[j%len(topics)]
|
topic := topics[j%len(topics)]
|
||||||
|
|
||||||
@@ -136,10 +136,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询最新的 10 条记录
|
// 查询最新的 10 条记录
|
||||||
querySize := 10
|
querySize := min(count, 10)
|
||||||
if count < querySize {
|
|
||||||
querySize = count
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = processor.QueryNewest(count-1, querySize)
|
_, err = processor.QueryNewest(count-1, querySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,7 +177,7 @@ func main() {
|
|||||||
go func(topicName string) {
|
go func(topicName string) {
|
||||||
defer mixWg.Done()
|
defer mixWg.Done()
|
||||||
|
|
||||||
for j := 0; j < 1000; j++ {
|
for j := range 1000 {
|
||||||
data := fmt.Sprintf("mix-%s-msg-%d", topicName, j)
|
data := fmt.Sprintf("mix-%s-msg-%d", topicName, j)
|
||||||
|
|
||||||
if _, err := seq.Write(topicName, []byte(data)); err == nil {
|
if _, err := seq.Write(topicName, []byte(data)); err == nil {
|
||||||
@@ -194,12 +191,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动查询 goroutine
|
// 启动查询 goroutine
|
||||||
for i := 0; i < 10; i++ {
|
for i := range 10 {
|
||||||
mixWg.Add(1)
|
mixWg.Add(1)
|
||||||
go func(queryID int) {
|
go func(queryID int) {
|
||||||
defer mixWg.Done()
|
defer mixWg.Done()
|
||||||
|
|
||||||
for j := 0; j < 200; j++ {
|
for j := range 200 {
|
||||||
topic := topics[j%len(topics)]
|
topic := topics[j%len(topics)]
|
||||||
|
|
||||||
processor, err := seq.GetProcessor(topic)
|
processor, err := seq.GetProcessor(topic)
|
||||||
@@ -267,7 +264,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 持续查询 goroutine
|
// 持续查询 goroutine
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
stressWg.Add(1)
|
stressWg.Add(1)
|
||||||
go func(queryID int) {
|
go func(queryID int) {
|
||||||
defer stressWg.Done()
|
defer stressWg.Done()
|
||||||
@@ -296,8 +293,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 进度显示 goroutine
|
// 进度显示 goroutine
|
||||||
stressWg.Add(1)
|
stressWg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer stressWg.Done()
|
defer stressWg.Done()
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -314,7 +310,7 @@ func main() {
|
|||||||
stressQueryCount.Load())
|
stressQueryCount.Load())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
|
|
||||||
stressWg.Wait()
|
stressWg.Wait()
|
||||||
stressDuration := time.Since(stressTestStart)
|
stressDuration := time.Since(stressTestStart)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// ===== TopicProcessor 作为聚合器使用 =====
|
// ===== TopicProcessor 作为聚合器使用 =====
|
||||||
fmt.Println("=== TopicProcessor 聚合器示例 ===\n")
|
fmt.Println("=== TopicProcessor 聚合器示例 ===")
|
||||||
|
|
||||||
// 创建 TopicProcessor(提供空 handler)
|
// 创建 TopicProcessor(提供空 handler)
|
||||||
logger := slog.Default()
|
logger := slog.Default()
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentTopic = null;
|
let currentTopic = null;
|
||||||
let displayedOffsets = new Set(); // 追踪已显示的日志偏移量
|
let currentCenterIndex = null; // 追踪当前中心索引位置
|
||||||
|
|
||||||
// 加载 topics
|
// 加载 topics
|
||||||
async function loadTopics() {
|
async function loadTopics() {
|
||||||
@@ -363,7 +363,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 选择 topic
|
// 选择 topic
|
||||||
function selectTopic(topic) {
|
function selectTopic(topic) {
|
||||||
currentTopic = topic;
|
currentTopic = topic;
|
||||||
displayedOffsets.clear(); // 切换 topic 时清空已显示记录
|
currentCenterIndex = null; // 切换 topic 时重置索引
|
||||||
|
|
||||||
// 更新选中状态
|
// 更新选中状态
|
||||||
document.querySelectorAll('.topic-item').forEach(item => {
|
document.querySelectorAll('.topic-item').forEach(item => {
|
||||||
@@ -393,32 +393,37 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载日志
|
// 加载日志
|
||||||
async function loadLogs() {
|
async function loadLogs(index) {
|
||||||
if (!currentTopic) return;
|
if (!currentTopic) return;
|
||||||
|
|
||||||
const backward = document.getElementById('backwardCount').value;
|
const backward = document.getElementById('backwardCount').value;
|
||||||
const forward = document.getElementById('forwardCount').value;
|
const forward = document.getElementById('forwardCount').value;
|
||||||
|
|
||||||
const response = await fetch('/api/query?topic=' + currentTopic +
|
// 构建查询 URL
|
||||||
'&backward=' + backward + '&forward=' + forward);
|
let url = '/api/query?topic=' + currentTopic +
|
||||||
|
'&backward=' + backward + '&forward=' + forward;
|
||||||
|
|
||||||
|
// 如果指定了索引,添加到查询参数
|
||||||
|
if (index !== undefined && index !== null) {
|
||||||
|
url += '&index=' + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 更新当前中心索引
|
||||||
|
currentCenterIndex = data.centerIndex;
|
||||||
|
|
||||||
const container = document.getElementById('logContainer');
|
const container = document.getElementById('logContainer');
|
||||||
|
|
||||||
if (data.records.length === 0 && displayedOffsets.size === 0) {
|
if (data.records.length === 0) {
|
||||||
container.innerHTML = '<div class="loading">暂无日志</div>';
|
container.innerHTML = '<div class="loading">暂无日志</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤出新记录
|
// 清空并重新渲染所有记录
|
||||||
const newRecords = data.records.filter(r => !displayedOffsets.has(r.offset));
|
const html = data.records.map(r => {
|
||||||
|
// 解析状态
|
||||||
if (newRecords.length > 0) {
|
|
||||||
// 生成新记录的 HTML
|
|
||||||
const newHTML = newRecords.map(r => {
|
|
||||||
displayedOffsets.add(r.offset); // 标记为已显示
|
|
||||||
|
|
||||||
// 解析状态,处理可能的状态值
|
|
||||||
let statusClass = 'pending';
|
let statusClass = 'pending';
|
||||||
let statusText = '待处理';
|
let statusText = '待处理';
|
||||||
let badgeClass = 'status-pending';
|
let badgeClass = 'status-pending';
|
||||||
@@ -433,26 +438,41 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
badgeClass = 'status-processing';
|
badgeClass = 'status-processing';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<div class="log-entry ' + statusClass + '" data-offset="' + r.offset + '">' +
|
return '<div class="log-entry ' + statusClass + '" data-index="' + r.index + '">' +
|
||||||
'<span class="status-badge ' + badgeClass + '">' + statusText + '</span>' +
|
'<span class="status-badge ' + badgeClass + '">[#' + r.index + '] ' + statusText + '</span>' +
|
||||||
r.data +
|
r.data +
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// 追加新记录
|
container.innerHTML = html;
|
||||||
container.innerHTML += newHTML;
|
|
||||||
|
|
||||||
// 自动滚动到底部
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryBackward() {
|
function queryBackward() {
|
||||||
|
if (currentCenterIndex === null || currentCenterIndex === undefined) {
|
||||||
loadLogs();
|
loadLogs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backward = parseInt(document.getElementById('backwardCount').value);
|
||||||
|
const forward = parseInt(document.getElementById('forwardCount').value);
|
||||||
|
|
||||||
|
// 向前翻页:中心索引向前移动(backward + forward + 1)个位置
|
||||||
|
const newIndex = currentCenterIndex - (backward + forward + 1);
|
||||||
|
loadLogs(newIndex >= 0 ? newIndex : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryForward() {
|
function queryForward() {
|
||||||
|
if (currentCenterIndex === null || currentCenterIndex === undefined) {
|
||||||
loadLogs();
|
loadLogs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backward = parseInt(document.getElementById('backwardCount').value);
|
||||||
|
const forward = parseInt(document.getElementById('forwardCount').value);
|
||||||
|
|
||||||
|
// 向后翻页:中心索引向后移动(backward + forward + 1)个位置
|
||||||
|
const newIndex = currentCenterIndex + (backward + forward + 1);
|
||||||
|
loadLogs(newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
@@ -509,6 +529,8 @@ func handleQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取查询参数
|
||||||
|
indexParam := r.URL.Query().Get("index")
|
||||||
backward, _ := strconv.Atoi(r.URL.Query().Get("backward"))
|
backward, _ := strconv.Atoi(r.URL.Query().Get("backward"))
|
||||||
forward, _ := strconv.Atoi(r.URL.Query().Get("forward"))
|
forward, _ := strconv.Atoi(r.URL.Query().Get("forward"))
|
||||||
|
|
||||||
@@ -526,45 +548,72 @@ func handleQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前处理索引和读取索引
|
// 获取记录总数
|
||||||
|
totalCount := processor.GetRecordCount()
|
||||||
|
|
||||||
|
// 确定查询中心索引
|
||||||
|
var centerIndex int
|
||||||
|
if indexParam == "" {
|
||||||
|
// 如果没有指定索引,使用当前处理位置
|
||||||
|
centerIndex = seq.GetProcessingIndex(topic)
|
||||||
|
} else {
|
||||||
|
centerIndex, _ = strconv.Atoi(indexParam)
|
||||||
|
// 限制索引范围
|
||||||
|
if centerIndex < 0 {
|
||||||
|
centerIndex = 0
|
||||||
|
}
|
||||||
|
if centerIndex >= totalCount {
|
||||||
|
centerIndex = totalCount - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前处理索引和读取索引(用于状态判断)
|
||||||
startIdx := seq.GetProcessingIndex(topic)
|
startIdx := seq.GetProcessingIndex(topic)
|
||||||
endIdx := seq.GetReadIndex(topic)
|
endIdx := seq.GetReadIndex(topic)
|
||||||
|
|
||||||
// 合并查询结果:向后 + 当前 + 向前
|
// 合并查询结果:向后 + 当前 + 向前
|
||||||
var results []*seqlog.RecordWithStatus
|
var results []*seqlog.RecordWithStatus
|
||||||
|
|
||||||
// 向后查询
|
// 向后查询(查询更早的记录)
|
||||||
if backward > 0 && startIdx > 0 {
|
if backward > 0 && centerIndex > 0 {
|
||||||
backResults, err := processor.QueryNewest(startIdx-1, backward)
|
backResults, err := processor.QueryNewest(centerIndex-1, backward)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
results = append(results, backResults...)
|
results = append(results, backResults...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前位置
|
// 当前位置的记录
|
||||||
if startIdx < endIdx {
|
if centerIndex >= 0 && centerIndex < totalCount {
|
||||||
currentResults, err := processor.QueryOldest(startIdx, 1)
|
currentResults, err := processor.QueryOldest(centerIndex, 1)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
results = append(results, currentResults...)
|
results = append(results, currentResults...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向前查询
|
// 向前查询(查询更新的记录)
|
||||||
if forward > 0 {
|
if forward > 0 && centerIndex+1 < totalCount {
|
||||||
forwardResults, err := processor.QueryOldest(endIdx, forward)
|
forwardResults, err := processor.QueryOldest(centerIndex+1, forward)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
results = append(results, forwardResults...)
|
results = append(results, forwardResults...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Record struct {
|
type Record struct {
|
||||||
|
Index int `json:"index"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算每条记录的实际索引位置
|
||||||
|
startRecordIndex := centerIndex - backward
|
||||||
|
if startRecordIndex < 0 {
|
||||||
|
startRecordIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
records := make([]Record, len(results))
|
records := make([]Record, len(results))
|
||||||
for i, r := range results {
|
for i, r := range results {
|
||||||
records[i] = Record{
|
records[i] = Record{
|
||||||
|
Index: startRecordIndex + i,
|
||||||
Status: r.Status.String(),
|
Status: r.Status.String(),
|
||||||
Data: string(r.Record.Data),
|
Data: string(r.Record.Data),
|
||||||
}
|
}
|
||||||
@@ -573,6 +622,10 @@ func handleQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"records": records,
|
"records": records,
|
||||||
"total": len(records),
|
"total": len(records),
|
||||||
|
"centerIndex": centerIndex,
|
||||||
|
"totalCount": totalCount,
|
||||||
|
"processingIndex": startIdx,
|
||||||
|
"readIndex": endIdx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user