Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/plugins/log/fake.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package log

import "context"

var _ Logger = (*TestLogger)(nil)

type TestLogger struct {
Expand Down Expand Up @@ -41,6 +43,10 @@ func (f *TestLogger) Error(msg string, ctx ...any) {
f.ErrorLogs.Ctx = ctx
}

func (f *TestLogger) FromContext(_ context.Context) Logger {
return NewTestLogger()
}

type Logs struct {
Calls int
Message string
Expand Down
37 changes: 21 additions & 16 deletions pkg/plugins/log/ifaces.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
package log

import "context"

// Logger is the default logger
type Logger interface {
// New returns a new contextual Logger that has this logger's context plus the given context.
New(ctx ...interface{}) Logger
New(ctx ...any) Logger

// Debug logs a message with debug level and key/value pairs, if any.
Debug(msg string, ctx ...interface{})
Debug(msg string, ctx ...any)

// Info logs a message with info level and key/value pairs, if any.
Info(msg string, ctx ...interface{})
Info(msg string, ctx ...any)

// Warn logs a message with warning level and key/value pairs, if any.
Warn(msg string, ctx ...interface{})
Warn(msg string, ctx ...any)

// Error logs a message with error level and key/value pairs, if any.
Error(msg string, ctx ...interface{})
Error(msg string, ctx ...any)

// FromContext returns a new contextual Logger that has this logger's context plus the given context.
FromContext(ctx context.Context) Logger
}

// PrettyLogger is used primarily to facilitate logging/user feedback for both
// the grafana-cli and the grafana backend when managing plugin installs
type PrettyLogger interface {
Successf(format string, args ...interface{})
Failuref(format string, args ...interface{})

Info(args ...interface{})
Infof(format string, args ...interface{})
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Successf(format string, args ...any)
Failuref(format string, args ...any)

Info(args ...any)
Infof(format string, args ...any)
Debug(args ...any)
Debugf(format string, args ...any)
Warn(args ...any)
Warnf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
}
12 changes: 12 additions & 0 deletions pkg/plugins/log/logger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package log

import (
"context"

"github.com/grafana/grafana/pkg/infra/log"
)

Expand Down Expand Up @@ -42,3 +44,13 @@ func (d *grafanaInfraLogWrapper) Warn(msg string, ctx ...any) {
func (d *grafanaInfraLogWrapper) Error(msg string, ctx ...any) {
d.l.Error(msg, ctx...)
}

func (d *grafanaInfraLogWrapper) FromContext(ctx context.Context) Logger {
concreteInfraLogger, ok := d.l.FromContext(ctx).(*log.ConcreteLogger)
if !ok {
return d.New()
}
return &grafanaInfraLogWrapper{
l: concreteInfraLogger,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package clientmiddleware

import (
"context"

"github.com/grafana/grafana-plugin-sdk-go/backend"

"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
)

// NewContextualLoggerMiddleware creates a new plugins.ClientMiddleware that adds
// a contextual logger to the request context.
func NewContextualLoggerMiddleware() plugins.ClientMiddleware {
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client {
return &ContextualLoggerMiddleware{
next: next,
}
})
}

type ContextualLoggerMiddleware struct {
next plugins.Client
}

// instrumentContext adds a contextual logger with plugin and request details to the given context.
func instrumentContext(ctx context.Context, endpoint string, pCtx backend.PluginContext) context.Context {
p := []any{"endpoint", endpoint, "pluginId", pCtx.PluginID}
if pCtx.DataSourceInstanceSettings != nil {
p = append(p, "dsName", pCtx.DataSourceInstanceSettings.Name)
p = append(p, "dsUID", pCtx.DataSourceInstanceSettings.UID)
}
if pCtx.User != nil {
p = append(p, "uname", pCtx.User.Login)
}
return log.WithContextualAttributes(ctx, p)
}

func (m *ContextualLoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ctx = instrumentContext(ctx, endpointQueryData, req.PluginContext)
return m.next.QueryData(ctx, req)
}

func (m *ContextualLoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
ctx = instrumentContext(ctx, endpointCallResource, req.PluginContext)
return m.next.CallResource(ctx, req, sender)
}

func (m *ContextualLoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
ctx = instrumentContext(ctx, endpointCheckHealth, req.PluginContext)
return m.next.CheckHealth(ctx, req)
}

func (m *ContextualLoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
ctx = instrumentContext(ctx, endpointCollectMetrics, req.PluginContext)
return m.next.CollectMetrics(ctx, req)
}
Comment on lines +39 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against nil requests to avoid panics in ContextualLoggerMiddleware

Each of these methods dereferences req (via req.PluginContext) without a nil check, unlike LoggerMiddleware, which explicitly handles req == nil. If any existing caller can pass a nil request, this middleware will now panic.

Recommend short‑circuiting to next when req is nil, mirroring LoggerMiddleware:

 func (m *ContextualLoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
-	ctx = instrumentContext(ctx, endpointQueryData, req.PluginContext)
-	return m.next.QueryData(ctx, req)
+	if req == nil {
+		return m.next.QueryData(ctx, req)
+	}
+	ctx = instrumentContext(ctx, endpointQueryData, req.PluginContext)
+	return m.next.QueryData(ctx, req)
 }
 
 func (m *ContextualLoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
-	ctx = instrumentContext(ctx, endpointCallResource, req.PluginContext)
-	return m.next.CallResource(ctx, req, sender)
+	if req == nil {
+		return m.next.CallResource(ctx, req, sender)
+	}
+	ctx = instrumentContext(ctx, endpointCallResource, req.PluginContext)
+	return m.next.CallResource(ctx, req, sender)
 }
 
 func (m *ContextualLoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
-	ctx = instrumentContext(ctx, endpointCheckHealth, req.PluginContext)
-	return m.next.CheckHealth(ctx, req)
+	if req == nil {
+		return m.next.CheckHealth(ctx, req)
+	}
+	ctx = instrumentContext(ctx, endpointCheckHealth, req.PluginContext)
+	return m.next.CheckHealth(ctx, req)
 }
 
 func (m *ContextualLoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
-	ctx = instrumentContext(ctx, endpointCollectMetrics, req.PluginContext)
-	return m.next.CollectMetrics(ctx, req)
+	if req == nil {
+		return m.next.CollectMetrics(ctx, req)
+	}
+	ctx = instrumentContext(ctx, endpointCollectMetrics, req.PluginContext)
+	return m.next.CollectMetrics(ctx, req)
 }

This keeps behavior consistent with the existing logger middleware and avoids introducing a crash surface.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *ContextualLoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ctx = instrumentContext(ctx, endpointQueryData, req.PluginContext)
return m.next.QueryData(ctx, req)
}
func (m *ContextualLoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
ctx = instrumentContext(ctx, endpointCallResource, req.PluginContext)
return m.next.CallResource(ctx, req, sender)
}
func (m *ContextualLoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
ctx = instrumentContext(ctx, endpointCheckHealth, req.PluginContext)
return m.next.CheckHealth(ctx, req)
}
func (m *ContextualLoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
ctx = instrumentContext(ctx, endpointCollectMetrics, req.PluginContext)
return m.next.CollectMetrics(ctx, req)
}
func (m *ContextualLoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if req == nil {
return m.next.QueryData(ctx, req)
}
ctx = instrumentContext(ctx, endpointQueryData, req.PluginContext)
return m.next.QueryData(ctx, req)
}
func (m *ContextualLoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
if req == nil {
return m.next.CallResource(ctx, req, sender)
}
ctx = instrumentContext(ctx, endpointCallResource, req.PluginContext)
return m.next.CallResource(ctx, req, sender)
}
func (m *ContextualLoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
if req == nil {
return m.next.CheckHealth(ctx, req)
}
ctx = instrumentContext(ctx, endpointCheckHealth, req.PluginContext)
return m.next.CheckHealth(ctx, req)
}
func (m *ContextualLoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
if req == nil {
return m.next.CollectMetrics(ctx, req)
}
ctx = instrumentContext(ctx, endpointCollectMetrics, req.PluginContext)
return m.next.CollectMetrics(ctx, req)
}
🤖 Prompt for AI Agents
In
pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go
around lines 39 to 57, the methods QueryData, CallResource, CheckHealth, and
CollectMetrics dereference req.PluginContext without checking if req is nil; add
a nil-check at the start of each method and short-circuit by calling and
returning m.next.<method>(ctx, req[, sender]) when req == nil (matching the
existing LoggerMiddleware behavior) to avoid panics and keep behavior
consistent.


func (m *ContextualLoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
return m.next.SubscribeStream(ctx, req)
}

func (m *ContextualLoggerMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
return m.next.PublishStream(ctx, req)
}

func (m *ContextualLoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
return m.next.RunStream(ctx, req, sender)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"time"

"github.com/grafana/grafana-plugin-sdk-go/backend"

"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
plog "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/setting"
Expand All @@ -33,10 +33,11 @@ type LoggerMiddleware struct {
logger plog.Logger
}

func (m *LoggerMiddleware) logRequest(ctx context.Context, pluginCtx backend.PluginContext, endpoint string, fn func(ctx context.Context) error) error {
func (m *LoggerMiddleware) logRequest(ctx context.Context, fn func(ctx context.Context) error) error {
status := statusOK
start := time.Now()
timeBeforePluginRequest := log.TimeSinceStart(ctx, start)

err := fn(ctx)
if err != nil {
status = statusError
Expand All @@ -48,31 +49,13 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, pluginCtx backend.Plu
logParams := []any{
"status", status,
"duration", time.Since(start),
"pluginId", pluginCtx.PluginID,
"endpoint", endpoint,
"eventName", "grafana-data-egress",
"time_before_plugin_request", timeBeforePluginRequest,
}

if pluginCtx.User != nil {
logParams = append(logParams, "uname", pluginCtx.User.Login)
}

traceID := tracing.TraceIDFromContext(ctx, false)
if traceID != "" {
logParams = append(logParams, "traceID", traceID)
}

if pluginCtx.DataSourceInstanceSettings != nil {
logParams = append(logParams, "dsName", pluginCtx.DataSourceInstanceSettings.Name)
logParams = append(logParams, "dsUID", pluginCtx.DataSourceInstanceSettings.UID)
}

if status == statusError {
logParams = append(logParams, "error", err)
}

m.logger.Info("Plugin Request Completed", logParams...)
m.logger.FromContext(ctx).Info("Plugin Request Completed", logParams...)
return err
}

Expand All @@ -82,7 +65,7 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData
}

var resp *backend.QueryDataResponse
err := m.logRequest(ctx, req.PluginContext, endpointQueryData, func(ctx context.Context) (innerErr error) {
err := m.logRequest(ctx, func(ctx context.Context) (innerErr error) {
resp, innerErr = m.next.QueryData(ctx, req)
return innerErr
})
Expand All @@ -95,7 +78,7 @@ func (m *LoggerMiddleware) CallResource(ctx context.Context, req *backend.CallRe
return m.next.CallResource(ctx, req, sender)
}

err := m.logRequest(ctx, req.PluginContext, endpointCallResource, func(ctx context.Context) (innerErr error) {
err := m.logRequest(ctx, func(ctx context.Context) (innerErr error) {
innerErr = m.next.CallResource(ctx, req, sender)
return innerErr
})
Expand All @@ -109,7 +92,7 @@ func (m *LoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHe
}

var resp *backend.CheckHealthResult
err := m.logRequest(ctx, req.PluginContext, endpointCheckHealth, func(ctx context.Context) (innerErr error) {
err := m.logRequest(ctx, func(ctx context.Context) (innerErr error) {
resp, innerErr = m.next.CheckHealth(ctx, req)
return innerErr
})
Expand All @@ -123,7 +106,7 @@ func (m *LoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.Coll
}

var resp *backend.CollectMetricsResult
err := m.logRequest(ctx, req.PluginContext, endpointCollectMetrics, func(ctx context.Context) (innerErr error) {
err := m.logRequest(ctx, func(ctx context.Context) (innerErr error) {
resp, innerErr = m.next.CollectMetrics(ctx, req)
return innerErr
})
Expand Down
Loading
Loading