You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Go蓝牙服务(ble-service)中读取journal日志

解决systemd服务运行时通过蓝牙流式传输自身日志的问题

我用Go编写了一个蓝牙服务ble-service,想要添加一个GATT特征,通过蓝牙流式传输该服务的日志。计划使用github.com/coreos/go-systemd/v22/sdjournal在GATT通知器中访问本地journal,读取日志直到无剩余,再等待新日志。

当前代码如下:

// +build linux

package service

import (
    "fmt"
    "regexp"
    "time"

    "github.com/coreos/go-systemd/v22/sdjournal"
    "github.com/paypal/gatt"
)

var (
    attrLogServiceUUID  = gatt.UUID16(0xD200)
    attrLogs            = BleAttribute{uuid: gatt.UUID16(0xD201), name: "Ble Service Logs"}
)

var messageRe = regexp.MustCompile(`"message"=>"(.?)"`)

func nextJournalLog(journal *sdjournal.Journal) (msg string, hasMore bool, err error) {
    i, err := journal.Next()
    if err != nil {
        return "", i == 0, err
    }
    if  i == 0 {
        return "", false, err
    }

    fmt.Println("Cursor ", i)
    entry, err := journal.GetEntry()
    if err != nil {
        return "", true, err
    }
    fmt.Println("Entry: ", entry)
    msgDataValue, err := journal.GetDataValue("MESSAGE")
    if err != nil {
        return "", true, err
    }
    fmt.Println("Message: ", msgDataValue)

    matches := messageRe.FindStringSubmatch(msgDataValue)
    fmt.Println(matches)
    if len(matches) > 1 {
        return matches[1], true, nil
    }
    return "", true, nil
}

func NewLogService() (s *gatt.Service) {
    s = newService(attrLogServiceUUID)
    
    logsChar := addCharacteristic(s, attrLogs)
    
    logsChar.HandleNotifyFunc(
        func(r gatt.Request, n gatt.Notifier) {
            defer logBLENotify(r, logsChar, "Logs")()

            journal, err := sdjournal.NewJournal()
            if err != nil {
                fmt.Println("Error creating journal", err)
                return
            }
            defer func() { journal.Close() }()

            err = journal.SeekTail()
            if err != nil {
                fmt.Println("Error seeking journal tail", err)
                return
            }
            cursor, err := journal.PreviousSkip(3)
            if err != nil {
                fmt.Println("Error seeking journal tail", err)
                return
            }
            fmt.Println("Cursor ", cursor)

            for !n.Done() {
                msg, hasMore, err := nextJournalLog(journal)
                time.Sleep(time.Second)
                if err != nil {
                    fmt.Println(err)
                    continue
                }
                if !hasMore {
                    fmt.Println("Waiting for more logs")
                    time.Sleep(3 * time.Second)
                    continue
                }
                _, err = n.Write([]byte(msg))
                if err != nil {
                    fmt.Println("Failed to write msg", msg)
                    continue
                }
            }
        })

        return
}

问题:作为systemd服务运行时,订阅日志特征看不到输出;直接运行可执行文件时,能看到上次作为systemd服务运行的journald日志。需要解决如何在服务运行时访问自身日志。


解决方案

1. 过滤当前服务的journal条目

默认sdjournal.NewJournal()会读取所有系统和用户的journal,必须添加过滤器,只获取当前ble-service的日志:

// 创建journal后添加过滤规则
err = journal.AddMatch("_SYSTEMD_UNIT=ble-service.service")
if err != nil {
    fmt.Println("Error adding systemd unit filter", err)
    return
}
// 额外添加PID过滤,确保只获取当前进程的日志
pid := os.Getpid()
err = journal.AddMatch(fmt.Sprintf("_PID=%d", pid))
if err != nil {
    fmt.Println("Error adding PID filter", err)
    return
}

2. 修正日志定位逻辑

原代码SeekTail()后PreviousSkip(3)可能跳到其他服务的日志尾部,改成定位到当前日志尾部,准备监听新日志:

err = journal.SeekTail()
if err != nil {
    fmt.Println("Error seeking journal tail", err)
    return
}
// 向前移动一个条目,确保Next()能获取后续新增的日志
_, err = journal.Previous()
if err != nil {
    fmt.Println("Error moving to previous entry", err)
    return
}

3. 移除多余的正则匹配

journal.GetDataValue("MESSAGE")直接返回原始日志内容,不需要解析JSON格式,删除正则表达式相关代码:

func nextJournalLog(journal *sdjournal.Journal) (msg string, hasMore bool, err error) {
    i, err := journal.Next()
    if err != nil {
        return "", i == 0, err
    }
    if i == 0 {
        return "", false, nil
    }

    msgDataValue, err := journal.GetDataValue("MESSAGE")
    if err != nil {
        return "", true, err
    }
    return msgDataValue, true, nil
}

4. 使用journal.Wait()替代睡眠等待

原代码的time.Sleep()效率低且可能错过日志,改用journal.Wait()等待新日志条目:

for !n.Done() {
    msg, hasMore, err := nextJournalLog(journal)
    if err != nil {
        fmt.Println(err)
        continue
    }
    if hasMore {
        _, err = n.Write([]byte(msg + "\n")) // 加换行提升可读性
        if err != nil {
            fmt.Println("Failed to write msg", msg)
            continue
        }
    } else {
        // 等待新日志,超时设为5秒避免永久阻塞
        waitErr := journal.Wait(5 * time.Second)
        if waitErr != nil && waitErr != sdjournal.ErrTimeout {
            fmt.Println("Error waiting for journal updates:", waitErr)
        }
    }
}

5. 确保服务有journal访问权限

如果服务以非root用户运行,需将用户加入systemd-journal组:

usermod -aG systemd-journal ble-service-user

同时修改systemd服务文件指定运行用户:

[Service]
User=ble-service-user
Group=ble-service-user

修改后的完整代码

// +build linux

package service

import (
    "fmt"
    "os"
    "time"

    "github.com/coreos/go-systemd/v22/sdjournal"
    "github.com/paypal/gatt"
)

var (
    attrLogServiceUUID = gatt.UUID16(0xD200)
    attrLogs           = BleAttribute{uuid: gatt.UUID16(0xD201), name: "Ble Service Logs"}
)

func nextJournalLog(journal *sdjournal.Journal) (msg string, hasMore bool, err error) {
    i, err := journal.Next()
    if err != nil {
        return "", i == 0, err
    }
    if i == 0 {
        return "", false, nil
    }

    msgDataValue, err := journal.GetDataValue("MESSAGE")
    if err != nil {
        return "", true, err
    }
    return msgDataValue, true, nil
}

func NewLogService() (s *gatt.Service) {
    s = newService(attrLogServiceUUID)

    logsChar := addCharacteristic(s, attrLogs)

    logsChar.HandleNotifyFunc(
        func(r gatt.Request, n gatt.Notifier) {
            defer logBLENotify(r, logsChar, "Logs")()

            journal, err := sdjournal.NewJournal()
            if err != nil {
                fmt.Println("Error creating journal", err)
                return
            }
            defer func() { journal.Close() }()

            // 添加过滤器,只获取当前服务的日志
            err = journal.AddMatch("_SYSTEMD_UNIT=ble-service.service")
            if err != nil {
                fmt.Println("Error adding systemd unit filter", err)
                return
            }
            // 添加PID过滤,确保只获取当前进程的日志
            pid := os.Getpid()
            err = journal.AddMatch(fmt.Sprintf("_PID=%d", pid))
            if err != nil {
                fmt.Println("Error adding PID filter", err)
                return
            }

            // 定位到日志尾部,准备监听新日志
            err = journal.SeekTail()
            if err != nil {
                fmt.Println("Error seeking journal tail", err)
                return
            }
            // 向前移动一个条目,确保Next()能获取后续新增日志
            _, err = journal.Previous()
            if err != nil {
                fmt.Println("Error moving to previous entry", err)
                return
            }

            for !n.Done() {
                msg, hasMore, err := nextJournalLog(journal)
                if err != nil {
                    fmt.Println(err)
                    continue
                }
                if hasMore {
                    _, err = n.Write([]byte(msg + "\n"))
                    if err != nil {
                        fmt.Println("Failed to write msg", msg)
                        continue
                    }
                } else {
                    // 等待新日志,超时5秒
                    waitErr := journal.Wait(5 * time.Second)
                    if waitErr != nil && waitErr != sdjournal.ErrTimeout {
                        fmt.Println("Error waiting for journal updates:", waitErr)
                    }
                }
            }
        })

    return s
}

内容的提问来源于stack exchange,提问作者Rowan Baker-French

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:08:05