Skip to content

Commit

Permalink
kms: add support for fetching server logs
Browse files Browse the repository at this point in the history
This commit adds support for fetching server logs
via the `/v1/log` API.

The log API returns a stream of log records. Each
record is a protobuf (or JSON) encoded message
containing the log leve, message, timestamp and
other information about a log event.

Clients can use a `LogRequest` to filter based on
log level, message, IP address etc.

Signed-off-by: Andreas Auernhammer <[email protected]>
  • Loading branch information
aead committed Jul 15, 2024
1 parent f921979 commit 54fcd1b
Show file tree
Hide file tree
Showing 12 changed files with 1,154 additions and 209 deletions.
42 changes: 42 additions & 0 deletions kms/client-examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ package kms_test
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"log/slog"
"time"

"github.com/minio/kms-go/kms"
)
Expand Down Expand Up @@ -264,3 +267,42 @@ func ExampleClient_ListEnclaves() {
fmt.Println(v.Name)
}
}

// ExampleClient_Logs shows how to fetch server log records.
func ExampleClient_Logs() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}

client, err := kms.NewClient(&kms.Config{
Endpoints: []string{
"127.0.0.1:7373",
},
APIKey: key,
TLS: &tls.Config{
RootCAs: nil, // Use nil for system root CAs or customize
InsecureSkipVerify: false, // Don't skip TLS cert verification in prod
},
})
if err != nil {
log.Fatalf("Failed to create KMS client: %v", err)
}

logs, err := client.Logs(context.TODO(), &kms.LogRequest{
Host: "127.0.0.1:7373", // The server to fetch logs from
Level: slog.LevelWarn, // Fetch only warnings or error logs
Since: time.Now().Add(-5 * time.Minute), // Fetch logs of the last 5 min
})
if err != nil {
log.Fatalf("Failed to fetch server logs: %v", err)
}
defer logs.Close()

for r, ok := logs.Next(); ok; r, ok = logs.Next() {
_ = r // TODO: print logs
}
if err = logs.Close(); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) {
log.Fatal(err)
}
}
62 changes: 62 additions & 0 deletions kms/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sync"
"time"

"aead.dev/mem"
"github.com/minio/kms-go/kms/cmds"
"github.com/minio/kms-go/kms/internal/api"
"github.com/minio/kms-go/kms/internal/headers"
Expand Down Expand Up @@ -722,6 +723,67 @@ func (c *Client) ReadDB(ctx context.Context) (*ReadDBResponse, error) {
}, nil
}

// Logs returns a stream of server log records from req.Host.
// If req.Host is empty, the first host of the client's host
// list is used.
//
// The LogRequest specifies which log records are fetched.
// For example, only records with a certain log level or
// records that contain a specific log message.
//
// It requires SysAdmin privileges.
//
// The returned error is of type *HostError.
func (c *Client) Logs(ctx context.Context, req *LogRequest) (*LogResponse, error) {
const (
Method = http.MethodPost
Path = api.PathLog
StatusOK = http.StatusOK
)

var (
err error
reqURL string
host = req.Host
)
if host == "" {
reqURL, host, err = c.lb.URL(Path)
} else {
reqURL, err = url.JoinPath(httpsURL(host), Path)
}
if err != nil {
return nil, hostError(host, err)
}

body, err := pb.Marshal(req)
if err != nil {
return nil, hostError(host, err)
}
r, err := http.NewRequestWithContext(ctx, Method, reqURL, bytes.NewReader(body))
if err != nil {
return nil, hostError(host, err)
}
r.Header.Add(headers.Accept, headers.ContentTypeBinary)
r.Header.Add(headers.ContentType, headers.ContentTypeBinary)

resp, err := c.client.Do(r)
if err != nil {
return nil, hostError(host, err)
}
if resp.StatusCode != StatusOK {
defer resp.Body.Close()
return nil, hostError(host, readError(resp))
}

if ct := resp.Header.Get(headers.ContentType); ct != headers.ContentTypeBinary {
return nil, hostError(host, fmt.Errorf("kms: invalid content-type '%s'", ct))
}
return &LogResponse{
r: resp.Body,
buf: make([]byte, 4*mem.KiB),
}, nil
}

// CreateEnclave creates a new enclave with the name req.Name.
//
// It returns ErrEnclaveExists if such an enclave already exists
Expand Down
1 change: 1 addition & 0 deletions kms/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
PathProfile = "/v1/debug/pprof"

PathDB = "/v1/db"
PathLog = "/v1/log"
PathKMS = "/v1/kms/"

PathRPCReplicate = "/v1/rpc/replicate"
Expand Down
142 changes: 142 additions & 0 deletions kms/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2024 - MinIO, Inc. All rights reserved.
// Use of this source code is governed by the AGPLv3
// license that can be found in the LICENSE file.

package kms

import (
"encoding/binary"
"errors"
"io"
"log/slog"
"net/netip"
"time"

pb "github.com/minio/kms-go/kms/protobuf"
)

// StackFrame contains the resolved file and line number
// of a function call.
type StackFrame struct {
// Function is the package path-qualified function name containing the
// source line. If non-empty, this string uniquely identifies a single
// function in the program. This may be the empty string if not known.
Function string

// File and Line are the file name and line number (1-based) of the source
// line. These may be the empty string and zero, respectively, if not known.
File string
Line int
}

// LogRecord is a structure representing a KMS log event.
type LogRecord struct {
// The log level of the event.
Level slog.Level

// The log message.
Message string

// The time at which the event was produced.
Time time.Time

// The stack trace at the time the event was recorded.
// Its first frame is the location at which this event
// was produced and subsequent frames represent function
// calls higher up the call stack.
//
// If empty, no stack trace has been captured.
Trace []StackFrame

// If non-empty, HTTP method of the request that caused
// this event.
Method string

// If non-empty, URL path of the request that caused
// this event.
Path string

// If non-empty, identity of the request that caused
// this event.
Identity Identity

// If valid, IP address of the client sending the
// request that caused this event.
IP netip.Addr
}

// MarshalPB converts the LogRecord into its protobuf representation.
func (r *LogRecord) MarshalPB(v *pb.LogRecord) error {
v.Level = int32(r.Level)
v.Message = r.Message
v.Time = pb.Time(r.Time)

if len(r.Trace) > 0 {
v.Trace = make([]*pb.LogRecord_StackFrame, 0, len(r.Trace))
for _, t := range r.Trace {
v.Trace = append(v.Trace, &pb.LogRecord_StackFrame{
Function: t.Function,
File: t.File,
Line: uint32(t.Line),
})
}
}
if r.Method != "" || r.Path != "" || r.Identity != "" || r.IP.IsValid() {
v.Req = &pb.LogRecord_Request{
Method: r.Method,
Path: r.Path,
Identity: r.Identity.String(),
IP: r.IP.String(),
}
}
return nil
}

// UnmarshalPB initializes the LogRecord from its protobuf representation.
func (r *LogRecord) UnmarshalPB(v *pb.LogRecord) error {
var ip netip.Addr
if v.Req != nil {
var err error
if ip, err = netip.ParseAddr(v.Req.IP); err != nil {
return err
}
}

r.Level = slog.Level(v.Level)
r.Message = v.Message
r.Time = v.Time.AsTime()

r.Trace = make([]StackFrame, 0, len(v.Trace))
for _, t := range v.GetTrace() {
r.Trace = append(r.Trace, StackFrame{
Function: t.Function,
File: t.File,
Line: int(t.Line),
})
}

r.Method = v.Req.GetMethod()
r.Path = v.Req.GetPath()
r.Identity = Identity(v.Req.GetIdentity())
r.IP = ip
return nil
}

// readLogRecord reads a length-encoded protobuf log record
// into buf and unmarshales it into rec. It returns the first
// error encountered while reading from r.
func readLogRecord(r io.Reader, buf []byte, rec *LogRecord) error {
if _, err := io.ReadFull(r, buf[:4]); err != nil {
return err
}

msgLen := binary.BigEndian.Uint32(buf)
if uint64(len(buf)) < uint64(msgLen) {
return errors.New("kms: log record too large")
}

if _, err := io.ReadFull(r, buf[:msgLen]); err != nil {
return err
}
return pb.Unmarshal(buf[:msgLen], rec)
}
Loading

0 comments on commit 54fcd1b

Please sign in to comment.