增强您的可观察性:将 Logrus 与 Grafana Loki 集成
在微服务和分布式系统的世界里,有效的日志记录不仅是一种便利,也是一种必要。今天,我将引导您将 Grafana Loki 与流行的 Go 日志库 logrus 集成,以创建一个强大的、可搜索的日志记录系统,从而显着提高您的可观察性能力。
Loki 集成的两种方式
有两种办法可以把日志发给 Loki:
- 使用 Promtail:此代理收集容器日志并将它们转发给 Loki
- 使用 logrus 中的 hook:这会捕获日志消息并通过 HTTP 将它们直接发送到 Loki
在本指南中,我们将重点介绍第二种方法,因为它简单且可以直接控制记录的内容。
理解 Logrus Hooks
Logrus 中的钩子是什么?
Logrus 中的钩子本质上是一个扩展点,允许您在创建日志条目时执行其他操作。钩子实现了一个简单的接口,其中包括 Fire()
(在创建日志条目时调用)和 Levels()
(定义应该为哪些日志级别触发此钩子)等方法。
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
这个简单但功能强大的界面使 Logrus 能够专注于核心日志记录功能,同时实现几乎无限的可扩展性。
在我们使用 Loki 的特定案例中,Hook 允许我们在创建日志条目时拦截它们并立即通过其 HTTP API 将它们发送到 Loki,而无需更改我们在应用程序代码中与 logger 交互的方式。
找到正确的钩子实现
经过广泛的研究(和几杯咖啡),我发现虽然没有专门用于 Loki-logrus
集成的官方 Golang 包,但有几个社区选项。最强大和最值得信赖的似乎是 YuKitsune/lokirus
,它提供了一个可靠的钩子实现,广泛用于生产环境。
了解 Loki 的基于标签的日志组织
在深入研究代码之前,重要的是要了解 Loki 使用标签组织日志,类似于 Prometheus 处理指标的方式。这种方法使日志高度可搜索和过滤,允许您快速深入到重要的事情。
对于我们的实现,我们将在日志中包含这些关键标签:
- 日志级别:info、warning、error、critical
- Request Path 请求路径
- HTTP method
- Message content 日志正文
- Request body 请求正文
- Request Duration 请求时长
- Request ID 链路追踪中的请求 ID
实施:完整的解决方案
让我们逐步构建一个健壮的日志系统。
第1步:创建请求跟踪中间件
首先,我们将为基于 Gin 的 API 创建一个中间件,该中间件捕获重要的请求详细信息并将它们添加到上下文中:
func RequestTracerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Capture request start time for duration calculation
startTime := time.Now()
// Extract basic request information
method := c.Request.Method
path := c.Request.URL.Path
// Capture request body while preserving it for further use
bodyBytes, _ := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// Store all captured information in the context
c.Set("requestBody", string(bodyBytes))
c.Set("path", path)
c.Set("method", method)
c.Set("start_time", startTime.UnixMilli())
// Continue request processing
c.Next()
}
}
该中间件捕获 HTTP 方法、路径、请求正文和请求的开始时间。通过将这些存储在 Gin 上下文中,我们可以稍后在日志代码中访问它们。
第2步:创建记录器接口
接下来,我们将定义一个干净的日志记录接口,抽象出实现细节:
package logging
import (
"context"
"github.com/sirupsen/logrus"
"github.com/yukitsune/lokirus"
)
// Logger defines the standard logging interface.
type Logger interface {
Debug(ctx context.Context, format string, args ...interface{})
Info(ctx context.Context, format string, args ...interface{})
Warn(ctx context.Context, format string, args ...interface{})
Error(ctx context.Context, format string, args ...interface{})
}
此接口提供了一个干净的抽象,易于在整个应用程序中使用。
第3步:使用 Loki Hook 初始化 Logger
现在,让我们创建一个函数来使用 Loki hook 初始化我们的 Logger:
func New(logtag string) Logger {
// Configure the Loki hook with appropriate options
opts := lokirus.NewLokiHookOptions().
// Map logrus panic level to Grafana's critical level
// See: https://grafana.com/docs/grafana/latest/explore/logs-integration/
WithLevelMap(lokirus.LevelMap{logrus.PanicLevel: "critical"}).
WithFormatter(&logrus.JSONFormatter{}).
WithStaticLabels(lokirus.Labels{
"job": "BE_SERVER1", // Service identifier
"tag": logtag, // Custom tag for grouping related logs
})
// Create the Loki hook with specified log levels
hook := lokirus.NewLokiHookWithOpts(
"http://127.0.0.1:3100", // Loki endpoint
opts,
logrus.InfoLevel,
logrus.WarnLevel,
logrus.ErrorLevel,
logrus.FatalLevel)
// Create and configure the logrus logger
l := logrus.New()
l.AddHook(hook)
l.SetFormatter(&logrus.JSONFormatter{})
// Return our implementation
return &LogrusLogger{
logger: l,
}
}
在此初始化中:
- 我们配置一个 Loki hook 来拦截日志条目
- 我们指定哪些日志级别应该触发 hook(info、warn、error、fatal)
- 我们设置了静态标签,将附加到每个日志条目
- 我们将 logrus 级别映射到 Loki 级别
- 我们将 hook 附加到 Logger 实例中
这种方法的美妙之处在于我们的记录器将继续正常运行,将日志写入其默认输出(通常是标准输出),并且它还会将这些日志发送到 Loki。如果 Loki 暂时不可用,我们的应用程序将继续在本地进行日志记录,没有问题。
第4步:实现 Logger 方法
最后,让我们实现实际的日志记录方法,这些方法将提取上下文信息并格式化我们的日志:
package logging
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
)
// LogrusLogger implements the Logger interface using Logrus.
type LogrusLogger struct {
logger *logrus.Logger
fields logrus.Fields
}
// Debug logs at debug level.
func (l *LogrusLogger) Debug(ctx context.Context, format string, args ...interface{}) {
entry := l.createEntry(ctx)
entry.Debugf(format, args...)
}
// Info logs at info level.
func (l *LogrusLogger) Info(ctx context.Context, format string, args ...interface{}) {
entry := l.createEntry(ctx)
entry.Infof(format, args...)
}
// Warn logs at warn level.
func (l *LogrusLogger) Warn(ctx context.Context, format string, args ...interface{}) {
entry := l.createEntry(ctx)
entry.Warnf(format, args...)
}
// Error logs at error level.
func (l *LogrusLogger) Error(ctx context.Context, format string, args ...interface{}) {
entry := l.createEntry(ctx)
entry.Errorf(format, args...)
}
// createEntry creates a logrus entry with fields from context.
func (l *LogrusLogger) createEntry(ctx context.Context) *logrus.Entry {
fields := copyFields(l.fields)
// Add request ID for tracing
if requestID, ok := ctx.Value("requestID").(string); ok {
fields["request_id"] = requestID
}
// Add HTTP request body for detailed debugging
if requestBody, ok := ctx.Value("requestBody").(string); ok {
fields["request_body"] = requestBody
}
// Add path for request identification
if path, ok := ctx.Value("path").(string); ok {
fields["path"] = path
}
// Add HTTP method for request type
if method, ok := ctx.Value("method").(string); ok {
fields["method"] = method
}
// Calculate request duration for performance monitoring
if startTime, ok := ctx.Value("start_time").(int64); ok {
duration := time.Now().UnixMilli() - startTime
fields["duration"] = fmt.Sprintf("%dms", duration)
}
return l.logger.WithFields(fields)
}
// copyFields creates a copy of the fields map to avoid modifying the original.
func copyFields(fields logrus.Fields) logrus.Fields {
newFields := logrus.Fields{}
for k, v := range fields {
newFields[k] = v
}
return newFields
}
稍作解释:
- 为不同的日志级别提供方法
- 从上下文中提取有用的信息
- 用适当的标签格式化它
- 计算性能监控的请求持续时间
当调用这些日志记录方法中的任何一个时,我们的 Loki hook 将拦截日志条目并将其发送给 Grafana Loki,其中包含我们添加的所有字段和上下文。
在应用程序中使用记录器
随着我们的实施完成,在您的应用程序中使用它很简单:
// Initialize at application startup
logger := logging.New("api-service")
// Use in your request handlers
func HandleRequest(c *gin.Context) {
// Create a context from the Gin context
ctx := c.Request.Context()
// Log request information
logger.Info(ctx, "Processing request")
// Your business logic here
// Log completion
logger.Info(ctx, "Request completed successfully")
}
配置 Grafana 以显示日志
日志数据进入 Loki 之后,就可以使用 Grafana 查看了,使用 logql:
{job="BE_SERVER1"} | json
这将从您的服务中提取所有日志并将它们解析为 JSON。然后,您可以添加过滤器:
{job="BE_SERVER1"} | json | path=~"/api/v1/.*" | duration > "100ms"
此查询查找处理时间超过100毫秒的对 API v1 端点的所有请求-非常适合识别慢速端点!
高级提示和技巧
1. 特殊情况下的自定义 Hook
当您需要特殊处理时,您可以创建自己的自定义 hook:
type CustomHook struct {
// Configuration fields here
}
func (h *CustomHook) Fire(entry *logrus.Entry) error {
// Custom processing logic
return nil
}
func (h *CustomHook) Levels() []logrus.Level {
return []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel}
}
这种模式对于与内部系统集成或对某些类型的日志实施专门处理特别有用。
2.使用逻辑运算符进行日志聚合
Loki 支持复杂查询的逻辑运算符:
{job="BE_SERVER1"} | json | level="error" or level="warn"
3.大容量服务的智能采样
对于高流量服务,请考虑对您的日志进行采样:
if rand.Float64() < 0.1 { // Log 10% of requests
logger.Info(ctx, "Sampled request details", "full_details", true)
}
4.用于增强可搜索性的结构化日志记录
始终更喜欢结构化日志记录而不是字符串连接:
// Good
logger.Info(ctx, "User authentication", "user_id", userID, "result", "success")
// Avoid
logger.Info(ctx, fmt.Sprintf("User %s authenticated successfully", userID))
结构化方法使以后过滤和搜索日志变得更加容易。
总结
通过这个实现,您现在拥有了一个强大的、可搜索的日志系统,该系统与 Grafana 和 Loki 无缝集成。通过利用 Logrus hook,我们在应用程序逻辑和日志基础架构之间建立了清晰的分离,使我们的系统更易于维护和灵活。
基于 hook 的方法提供:
- 集中式日志存储和可视化
- 强大的查询功能
- 每个日志条目的丰富上下文
- 嵌入在日志中的性能指标
- 对应用程序性能的影响最小
原文:https://dev.to/srrathi/supercharging-your-observability-integrating-logrus-with-grafana-loki-3f8a
enjoy :-)