Logging API 调用
在本节中,让我们简化日志拦截器。这与我们在上一节中所做的类似,不过这次我们将使用另一种中间件:日志中间件。
虽然这个中间件可以与许多不同的日志记录器集成,但我们将在 Golang 中使用默认的日志包。这样,它就可以很容易地与您最喜欢的日志记录器集成。
接下来的命令只有在您没有获取 |
首先,让我们获取这个中间件的依赖。在 server
文件夹中,我们运行以下命令:
$ go get github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging
现在,我们可以开始创建我们的日志记录器。我们将通过定义一个函数来创建它,该函数返回一个 loggerFunc
。这个函数的签名如下:
func(ctx context.Context, lvl logging.Level, msg string, fields ...any)
我们已经知道 context
是什么,但其余的内容是与日志记录器相关的。lvl
是日志级别,例如 Debug
、Info
、Warning
或 Error
,通常用来根据严重性过滤日志。然后,msg
是由日志记录中间件生成的消息,比如 ":started call"
或 ":finished call"
。这有助于我们理解日志的上下文。最后,fields
是我们打印有用日志所需的其他所有信息。在我们的例子中,我们将使用服务名称和方法名称。这将使我们能够创建如下的日志:
INFO :started call todo.v2.TodoService UpdateTasks
一个不容易理解的地方是 fields
参数。这是因为它呈现为一个任意类型的可变参数(vararg
)。实际上,我们可以将其转换为一个 map
,以便获取像 grpc.service
、grpc.method
等特定的字段名称。为了做到这一点,我们可以写出以下代码:
f := make(map[string]any, len(fields)/2)
i := logging.Fields(fields).Iterator()
for i.Next() {
k, v := i.At()
f[k] = v
}
注意,我们创建了一个长度为 len(fields)/2
的 map。这是因为在 fields
参数中,字段的名称和值是交替出现的。一个示例是:
grpc.service todo.v2.TodoService grpc.method ListTasks
您可以通过展开 vararg
打印 fields
来查看完整内容:
log.Println(fields...)
了解了这一点后,我们可以继续编写日志记录器。我们将创建一个名为 logCalls
的函数,它接受一个 log.Logger
(来自 Golang 标准库)作为参数,并返回一个 logging.Logger
(来自日志中间件)。日志记录器的逻辑将是检查日志级别,为消息添加级别前缀,然后将服务名称和方法名称附加到整个消息中:
import (
//...
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
)
const grpcService = "grpc.service"
const grpcMethod = "grpc.method"
func logCalls(l *log.Logger) logging.Logger {
return logging.LoggerFunc(func(_ context.Context, lvl logging.Level, msg string, fields ...any) {
f := make(map[string]any, len(fields)/2)
i := logging.Fields(fields).Iterator()
for i.Next() {
k, v := i.At()
f[k] = v
}
switch lvl {
case logging.LevelDebug:
msg = fmt.Sprintf("DEBUG :%v", msg)
case logging.LevelInfo:
msg = fmt.Sprintf("INFO :%v", msg)
case logging.LevelWarn:
msg = fmt.Sprintf("WARN :%v", msg)
case logging.LevelError:
msg = fmt.Sprintf("ERROR :%v", msg)
default:
panic(fmt.Sprintf("unknown level %v", lvl))
}
l.Println(msg, f[grpcService], f[grpcMethod])
})
}
现在,尽管这种方法总是准确的,因为我们可以从构建的 map
中检索键,但这意味着每次调用此拦截器时我们都需要构建一个 map
。这实际上并不高效。我想先向您展示完整的示例,之后再展示高效的方式,以便您理解如何使用 fields
参数。
为了提高效率,我们可以利用服务和方法总是分别位于索引 5 和 7 的事实。因此,我们将删除 map
创建部分,替换 grpcService
和 grpcMethod
为 5 和 7,并直接访问 fields
的第 5 和第 7 个元素:
const grpcService = 5
const grpcMethod = 7
func logCalls(l *log.Logger) logging.Logger {
return logging.LoggerFunc(func(_ context.Context, lvl logging.Level, msg string, fields ...any) {
// ...
l.Println(msg, fields[grpcService], fields[grpcMethod])
})
}
这样就更加高效了。值得注意的是,这样做的安全性较差。我们假设接收到的所有 fields
中,服务和方法总是位于相同的索引,并且我们的 fields
数组足够大。我们现在可以安全地这样假设,因为这些是始终按此顺序添加的常见字段。然而,如果库发生变化,您可能会遇到越界访问或者获取不同信息的情况,请对此保持警觉。
最后,我们需要做的就是注册拦截器。这与我们之前注册身份验证拦截器的方式类似,主要区别在于,现在我们需要创建一个日志记录器并将其传递给 logCalls
函数。我们将使用 Golang 的 log.Logger
,它会在消息前打印日期和时间。最后,我们将 logCalls
的结果传递给 logging.UnaryServerInterceptor
和 logging.StreamServerInterceptor
:
import (
//...
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
)
//...
func main() {
//...
logger := log.New(os.Stderr, "", log.Ldate|log.Ltime)
opts := []grpc.ServerOption{
//...
grpc.ChainUnaryInterceptor(
//...
logging.UnaryServerInterceptor(logCalls(logger)),
),
grpc.ChainStreamInterceptor(
//...
logging.StreamServerInterceptor(logCalls(logger)),
),
}
//...
}
之后,我们就可以运行我们的服务器:
$ go run ./server 0.0.0.0:50051
listening at 0.0.0.0:50051
在运行客户端之前,确保您已经将 |
然后运行客户端:
$ go run ./client 0.0.0.0:50051
如果我们检查运行服务器的终端,我们应该会看到如下信息:
INFO :started call todo.v2.TodoService ListTasks
INFO :finished call todo.v2.TodoService ListTasks
总结来说,我们看到与身份验证中间件类似,我们可以将一个日志记录器简单地添加到我们的 gRPC 服务器中。我们还看到,我们可以通过将 fields
varargs
转换为 map
来访问更多信息。最后,我们看到,由于某些字段总是位于 vararg
中的相同位置,因此我们可以直接通过索引访问这些信息,而不必每次都生成 map
。