🌐 AI搜索 & 代理 主页
Skip to content

Commit 4938a50

Browse files
committed
Add logging stack with slog adapter
This allows us to have a deeper observability stack in the MCP server, particularly for surfacing logging in the remote MCP server.
1 parent 82c4930 commit 4938a50

File tree

8 files changed

+253
-3
lines changed

8 files changed

+253
-3
lines changed

internal/ghmcp/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/github/github-mcp-server/pkg/github"
1818
"github.com/github/github-mcp-server/pkg/lockdown"
1919
mcplog "github.com/github/github-mcp-server/pkg/log"
20+
"github.com/github/github-mcp-server/pkg/observability"
21+
"github.com/github/github-mcp-server/pkg/observability/log"
2022
"github.com/github/github-mcp-server/pkg/raw"
2123
"github.com/github/github-mcp-server/pkg/translations"
2224
gogithub "github.com/google/go-github/v79/github"
@@ -150,6 +152,9 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
150152
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
151153
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, restClient, gqlHTTPClient))
152154

155+
slogAdapter := log.NewSlogLogger(cfg.Logger, log.InfoLevel)
156+
obsv := observability.NewExporters(slogAdapter)
157+
153158
// Create default toolsets
154159
tsg := github.DefaultToolsetGroup(
155160
cfg.ReadOnly,
@@ -160,6 +165,7 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
160165
cfg.ContentWindowSize,
161166
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
162167
repoAccessCache,
168+
obsv,
163169
)
164170

165171
// Enable and register toolsets if configured

pkg/github/issues.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"log/slog"
89
"net/http"
910
"strings"
1011
"time"
1112

1213
ghErrors "github.com/github/github-mcp-server/pkg/errors"
1314
"github.com/github/github-mcp-server/pkg/lockdown"
15+
"github.com/github/github-mcp-server/pkg/observability"
1416
"github.com/github/github-mcp-server/pkg/sanitize"
1517
"github.com/github/github-mcp-server/pkg/translations"
1618
"github.com/github/github-mcp-server/pkg/utils"
@@ -229,7 +231,7 @@ func fragmentToIssue(fragment IssueFragment) *github.Issue {
229231
}
230232

231233
// IssueRead creates a tool to get details of a specific issue in a GitHub repository.
232-
func IssueRead(getClient GetClientFn, getGQLClient GetGQLClientFn, cache *lockdown.RepoAccessCache, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
234+
func IssueRead(getClient GetClientFn, getGQLClient GetGQLClientFn, cache *lockdown.RepoAccessCache, t translations.TranslationHelperFunc, flags FeatureFlags, obsv *observability.Exporters) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
233235
schema := &jsonschema.Schema{
234236
Type: "object",
235237
Properties: map[string]*jsonschema.Schema{
@@ -304,6 +306,8 @@ Options are:
304306
return utils.NewToolResultErrorFromErr("failed to get GitHub graphql client", err), nil, nil
305307
}
306308

309+
obsv.Logger.Info("deciding issue read method", slog.String("owner", owner), slog.String("method", method))
310+
307311
switch method {
308312
case "get":
309313
result, err := GetIssue(ctx, client, cache, owner, repo, issueNumber, flags)

pkg/github/tools.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/github/github-mcp-server/pkg/lockdown"
9+
"github.com/github/github-mcp-server/pkg/observability"
910
"github.com/github/github-mcp-server/pkg/raw"
1011
"github.com/github/github-mcp-server/pkg/toolsets"
1112
"github.com/github/github-mcp-server/pkg/translations"
@@ -160,7 +161,16 @@ func GetDefaultToolsetIDs() []string {
160161
}
161162
}
162163

163-
func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc, contentWindowSize int, flags FeatureFlags, cache *lockdown.RepoAccessCache) *toolsets.ToolsetGroup {
164+
func DefaultToolsetGroup(readOnly bool,
165+
getClient GetClientFn,
166+
getGQLClient GetGQLClientFn,
167+
getRawClient raw.GetRawClientFn,
168+
t translations.TranslationHelperFunc,
169+
contentWindowSize int,
170+
flags FeatureFlags,
171+
cache *lockdown.RepoAccessCache,
172+
obsv *observability.Exporters,
173+
) *toolsets.ToolsetGroup {
164174
tsg := toolsets.NewToolsetGroup(readOnly)
165175

166176
// Define all available features with their default state (disabled)
@@ -200,7 +210,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
200210
)
201211
issues := toolsets.NewToolset(ToolsetMetadataIssues.ID, ToolsetMetadataIssues.Description).
202212
AddReadTools(
203-
toolsets.NewServerTool(IssueRead(getClient, getGQLClient, cache, t, flags)),
213+
toolsets.NewServerTool(IssueRead(getClient, getGQLClient, cache, t, flags, obsv)),
204214
toolsets.NewServerTool(SearchIssues(getClient, t)),
205215
toolsets.NewServerTool(ListIssues(getGQLClient, t)),
206216
toolsets.NewServerTool(ListIssueTypes(getClient, t)),

pkg/observability/log/level.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package log
2+
3+
// Level represents the log level, from debug to fatal
4+
type Level struct {
5+
level string
6+
}
7+
8+
var (
9+
// DebugLevel causes all logs to be logged
10+
DebugLevel = Level{"debug"}
11+
// InfoLevel causes all logs of level info or more severe to be logged
12+
InfoLevel = Level{"info"}
13+
// WarnLevel causes all logs of level warn or more severe to be logged
14+
WarnLevel = Level{"warn"}
15+
// ErrorLevel causes all logs of level error or more severe to be logged
16+
ErrorLevel = Level{"error"}
17+
// FatalLevel causes only logs of level fatal to be logged
18+
FatalLevel = Level{"fatal"}
19+
)
20+
21+
// String returns the string representation for Level
22+
//
23+
// This is useful when trying to get the string values for Level and mapping level in other external libraries. For example:
24+
// ```
25+
// trace.SetLogLevel(kvp.String("loglevel", log.DebugLevel.String())
26+
// ```
27+
func (l Level) String() string {
28+
return l.level
29+
}

pkg/observability/log/log.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package log
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
)
7+
8+
type Logger interface {
9+
Log(ctx context.Context, level Level, msg string, fields ...slog.Attr)
10+
Debug(msg string, fields ...slog.Attr)
11+
Info(msg string, fields ...slog.Attr)
12+
Warn(msg string, fields ...slog.Attr)
13+
Error(msg string, fields ...slog.Attr)
14+
Fatal(msg string, fields ...slog.Attr)
15+
WithFields(fields ...slog.Attr) Logger
16+
WithError(err error) Logger
17+
Named(name string) Logger
18+
WithLevel(level Level) Logger
19+
Sync() error
20+
Level() Level
21+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package log
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
)
7+
8+
type NoopLogger struct{}
9+
10+
var _ Logger = (*NoopLogger)(nil)
11+
12+
func NewNoopLogger() *NoopLogger {
13+
return &NoopLogger{}
14+
}
15+
16+
func (l *NoopLogger) Level() Level {
17+
return DebugLevel
18+
}
19+
20+
func (l *NoopLogger) Log(ctx context.Context, level Level, msg string, fields ...slog.Attr) {
21+
// No-op
22+
}
23+
24+
func (l *NoopLogger) Debug(msg string, fields ...slog.Attr) {
25+
// No-op
26+
}
27+
28+
func (l *NoopLogger) Info(msg string, fields ...slog.Attr) {
29+
// No-op
30+
}
31+
32+
func (l *NoopLogger) Warn(msg string, fields ...slog.Attr) {
33+
// No-op
34+
}
35+
36+
func (l *NoopLogger) Error(msg string, fields ...slog.Attr) {
37+
// No-op
38+
}
39+
40+
func (l *NoopLogger) Fatal(msg string, fields ...slog.Attr) {
41+
// No-op
42+
}
43+
44+
func (l *NoopLogger) WithFields(fields ...slog.Attr) Logger {
45+
return l
46+
}
47+
48+
func (l *NoopLogger) WithError(err error) Logger {
49+
return l
50+
}
51+
52+
func (l *NoopLogger) Named(name string) Logger {
53+
return l
54+
}
55+
56+
func (l *NoopLogger) WithLevel(level Level) Logger {
57+
return l
58+
}
59+
60+
func (l *NoopLogger) Sync() error {
61+
return nil
62+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package log
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
)
7+
8+
type SlogLogger struct {
9+
logger *slog.Logger
10+
level Level
11+
}
12+
13+
func NewSlogLogger(logger *slog.Logger, level Level) *SlogLogger {
14+
return &SlogLogger{
15+
logger: logger,
16+
level: level,
17+
}
18+
}
19+
20+
func (l *SlogLogger) Level() Level {
21+
return l.level
22+
}
23+
24+
func (l *SlogLogger) Log(ctx context.Context, level Level, msg string, fields ...slog.Attr) {
25+
slogLevel := convertLevel(level)
26+
l.logger.LogAttrs(ctx, slogLevel, msg, fields...)
27+
}
28+
29+
func (l *SlogLogger) Debug(msg string, fields ...slog.Attr) {
30+
l.Log(context.Background(), DebugLevel, msg, fields...)
31+
}
32+
33+
func (l *SlogLogger) Info(msg string, fields ...slog.Attr) {
34+
l.Log(context.Background(), InfoLevel, msg, fields...)
35+
}
36+
37+
func (l *SlogLogger) Warn(msg string, fields ...slog.Attr) {
38+
l.Log(context.Background(), WarnLevel, msg, fields...)
39+
}
40+
41+
func (l *SlogLogger) Error(msg string, fields ...slog.Attr) {
42+
l.Log(context.Background(), ErrorLevel, msg, fields...)
43+
}
44+
45+
func (l *SlogLogger) Fatal(msg string, fields ...slog.Attr) {
46+
l.Log(context.Background(), FatalLevel, msg, fields...)
47+
panic("fatal log called")
48+
}
49+
50+
func (l *SlogLogger) WithFields(fields ...slog.Attr) Logger {
51+
fieldKvPairs := make([]any, 0, len(fields)*2)
52+
for _, attr := range fields {
53+
k, v := attr.Key, attr.Value
54+
fieldKvPairs = append(fieldKvPairs, k, v.Any())
55+
}
56+
return &SlogLogger{
57+
logger: l.logger.With(fieldKvPairs...),
58+
level: l.level,
59+
}
60+
}
61+
62+
func (l *SlogLogger) WithError(err error) Logger {
63+
return &SlogLogger{
64+
logger: l.logger.With("error", err.Error()),
65+
level: l.level,
66+
}
67+
}
68+
69+
func (l *SlogLogger) Named(name string) Logger {
70+
return &SlogLogger{
71+
logger: l.logger.With("logger", name),
72+
level: l.level,
73+
}
74+
}
75+
76+
func (l *SlogLogger) WithLevel(level Level) Logger {
77+
return &SlogLogger{
78+
logger: l.logger,
79+
level: level,
80+
}
81+
}
82+
83+
func (l *SlogLogger) Sync() error {
84+
// Slog does not require syncing
85+
return nil
86+
}
87+
88+
func convertLevel(level Level) slog.Level {
89+
switch level {
90+
case DebugLevel:
91+
return slog.LevelDebug
92+
case InfoLevel:
93+
return slog.LevelInfo
94+
case WarnLevel:
95+
return slog.LevelWarn
96+
case ErrorLevel:
97+
return slog.LevelError
98+
case FatalLevel:
99+
return slog.LevelError
100+
default:
101+
return slog.LevelInfo
102+
}
103+
}

pkg/observability/observability.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package observability
2+
3+
import (
4+
"github.com/github/github-mcp-server/pkg/observability/log"
5+
)
6+
7+
type Exporters struct {
8+
Logger log.Logger
9+
}
10+
11+
func NewExporters(logger log.Logger) *Exporters {
12+
return &Exporters{
13+
Logger: logger,
14+
}
15+
}

0 commit comments

Comments
 (0)