mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Logger: Add feature toggle for errors in HTTP request logs (#64425)
This commit is contained in:
@@ -102,6 +102,7 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. |
|
||||
| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. |
|
||||
| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. |
|
||||
| `unifiedRequestLog` | Writes error logs to the request logger |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
||||
@@ -90,4 +90,5 @@ export interface FeatureToggles {
|
||||
alertStateHistoryLokiSecondary?: boolean;
|
||||
alertStateHistoryLokiPrimary?: boolean;
|
||||
alertStateHistoryLokiOnly?: boolean;
|
||||
unifiedRequestLog?: boolean;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/middleware/csrf"
|
||||
"github.com/grafana/grafana/pkg/middleware/loggermw"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
@@ -148,6 +149,7 @@ type HTTPServer struct {
|
||||
httpEntityStore httpentitystore.HTTPEntityStore
|
||||
SearchV2HTTPService searchV2.SearchHTTPService
|
||||
ContextHandler *contexthandler.ContextHandler
|
||||
LoggerMiddleware loggermw.Logger
|
||||
SQLStore db.DB
|
||||
AlertEngine *alerting.AlertEngine
|
||||
AlertNG *ngalert.AlertNG
|
||||
@@ -226,7 +228,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
loginService login.Service, authenticator loginpkg.Authenticator, accessControl accesscontrol.AccessControl,
|
||||
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
||||
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
||||
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
||||
contextHandler *contexthandler.ContextHandler, loggerMiddleware loggermw.Logger, features *featuremgmt.FeatureManager,
|
||||
alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
||||
quotaService quota.Service, socialService social.Service, tracer tracing.Tracer,
|
||||
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
|
||||
@@ -296,6 +298,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
LivePushGateway: livePushGateway,
|
||||
PluginContextProvider: plugCtxProvider,
|
||||
ContextHandler: contextHandler,
|
||||
LoggerMiddleware: loggerMiddleware,
|
||||
AlertNG: alertNG,
|
||||
LibraryPanelService: libraryPanelService,
|
||||
LibraryElementService: libraryElementService,
|
||||
@@ -579,7 +582,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
m.Use(middleware.RequestTracing(hs.tracer))
|
||||
m.Use(middleware.RequestMetrics(hs.Features))
|
||||
|
||||
m.UseMiddleware(middleware.Logger(hs.Cfg))
|
||||
m.UseMiddleware(hs.LoggerMiddleware.Middleware())
|
||||
|
||||
if hs.Cfg.EnableGzip {
|
||||
m.UseMiddleware(middleware.Gziper())
|
||||
|
||||
@@ -80,21 +80,11 @@ func (r *NormalResponse) ErrMessage() string {
|
||||
|
||||
func (r *NormalResponse) WriteTo(ctx *contextmodel.ReqContext) {
|
||||
if r.err != nil {
|
||||
v := map[string]interface{}{}
|
||||
traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false)
|
||||
if err := json.Unmarshal(r.body.Bytes(), &v); err == nil {
|
||||
v["traceID"] = traceID
|
||||
if b, err := json.Marshal(v); err == nil {
|
||||
r.body = bytes.NewBuffer(b)
|
||||
}
|
||||
if errutil.HasUnifiedLogging(ctx.Req.Context()) {
|
||||
ctx.Error = r.err
|
||||
} else {
|
||||
r.writeLogLine(ctx)
|
||||
}
|
||||
|
||||
logger := ctx.Logger.Error
|
||||
var gfErr *errutil.Error
|
||||
if errors.As(r.err, &gfErr) {
|
||||
logger = gfErr.LogLevel.LogFunc(ctx.Logger)
|
||||
}
|
||||
logger(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr(), "traceID", traceID)
|
||||
}
|
||||
|
||||
header := ctx.Resp.Header()
|
||||
@@ -107,6 +97,24 @@ func (r *NormalResponse) WriteTo(ctx *contextmodel.ReqContext) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NormalResponse) writeLogLine(c *contextmodel.ReqContext) {
|
||||
v := map[string]interface{}{}
|
||||
traceID := tracing.TraceIDFromContext(c.Req.Context(), false)
|
||||
if err := json.Unmarshal(r.body.Bytes(), &v); err == nil {
|
||||
v["traceID"] = traceID
|
||||
if b, err := json.Marshal(v); err == nil {
|
||||
r.body = bytes.NewBuffer(b)
|
||||
}
|
||||
}
|
||||
|
||||
logger := c.Logger.Error
|
||||
var gfErr errutil.Error
|
||||
if errors.As(r.err, &gfErr) {
|
||||
logger = gfErr.LogLevel.LogFunc(c.Logger)
|
||||
}
|
||||
logger(r.errMessage, "error", r.err, "remote_addr", c.RemoteAddr(), "traceID", traceID)
|
||||
}
|
||||
|
||||
func (r *NormalResponse) SetHeader(key, value string) *NormalResponse {
|
||||
r.header.Set(key, value)
|
||||
return r
|
||||
@@ -253,8 +261,8 @@ func Error(status int, message string, err error) *NormalResponse {
|
||||
|
||||
// Err creates an error response based on an errutil.Error error.
|
||||
func Err(err error) *NormalResponse {
|
||||
grafanaErr := &errutil.Error{}
|
||||
if !errors.As(err, grafanaErr) {
|
||||
grafanaErr := errutil.Error{}
|
||||
if !errors.As(err, &grafanaErr) {
|
||||
return Error(http.StatusInternalServerError, "", fmt.Errorf("unexpected error type [%s]: %w", reflect.TypeOf(err), err))
|
||||
}
|
||||
|
||||
@@ -273,8 +281,8 @@ func Err(err error) *NormalResponse {
|
||||
// rename this to Error when we're confident that that would be safe to
|
||||
// do.
|
||||
func ErrOrFallback(status int, message string, err error) *NormalResponse {
|
||||
grafanaErr := &errutil.Error{}
|
||||
if errors.As(err, grafanaErr) {
|
||||
grafanaErr := errutil.Error{}
|
||||
if errors.As(err, &grafanaErr) {
|
||||
return Err(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 Unknwon
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func Logger(cfg *setting.Cfg) web.Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// we have to init the context with the counter here to update the request
|
||||
r = r.WithContext(log.InitCounter(r.Context()))
|
||||
// put the start time on context so we can measure it later.
|
||||
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
|
||||
|
||||
rw := web.Rw(w, r)
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
timeTaken := time.Since(start) / time.Millisecond
|
||||
duration := time.Since(start).String()
|
||||
ctx := contexthandler.FromContext(r.Context())
|
||||
if ctx != nil && ctx.PerfmonTimer != nil {
|
||||
ctx.PerfmonTimer.Observe(float64(timeTaken))
|
||||
}
|
||||
|
||||
status := rw.Status()
|
||||
if status == 200 || status == 304 {
|
||||
if !cfg.RouterLogging {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
logParams := []interface{}{
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", status,
|
||||
"remote_addr", ctx.RemoteAddr(),
|
||||
"time_ms", int64(timeTaken),
|
||||
"duration", duration,
|
||||
"size", rw.Size(),
|
||||
"referer", SanitizeURL(ctx, r.Referer()),
|
||||
}
|
||||
|
||||
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagDatabaseMetrics) {
|
||||
logParams = append(logParams, "db_call_count", log.TotalDBCallCount(ctx.Req.Context()))
|
||||
}
|
||||
|
||||
if handler, exist := routeOperationName(ctx.Req); exist {
|
||||
logParams = append(logParams, "handler", handler)
|
||||
}
|
||||
|
||||
if status >= 500 {
|
||||
ctx.Logger.Error("Request Completed", logParams...)
|
||||
} else {
|
||||
ctx.Logger.Info("Request Completed", logParams...)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var sensitiveQueryStrings = [...]string{
|
||||
"auth_token",
|
||||
}
|
||||
|
||||
func SanitizeURL(ctx *contextmodel.ReqContext, s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
ctx.Logger.Warn("Received invalid referer in request headers, removed for log forgery prevention")
|
||||
return ""
|
||||
}
|
||||
|
||||
// strip out sensitive query strings
|
||||
values := u.Query()
|
||||
for _, query := range sensitiveQueryStrings {
|
||||
values.Del(query)
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
)
|
||||
|
||||
func Test_sanitizeURL(t *testing.T) {
|
||||
type args struct {
|
||||
ctx *contextmodel.ReqContext
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Receiving empty string should return it",
|
||||
args: args{
|
||||
ctx: &contextmodel.ReqContext{
|
||||
Logger: log.New("test.logger"),
|
||||
},
|
||||
s: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "Receiving valid URL string should return it parsed",
|
||||
args: args{
|
||||
ctx: &contextmodel.ReqContext{
|
||||
Logger: log.New("test.logger"),
|
||||
},
|
||||
s: "https://grafana.com/",
|
||||
},
|
||||
want: "https://grafana.com/",
|
||||
},
|
||||
{
|
||||
name: "Receiving invalid URL string should return empty string",
|
||||
args: args{
|
||||
ctx: &contextmodel.ReqContext{
|
||||
Logger: log.New("test.logger"),
|
||||
},
|
||||
s: "this is not a valid URL",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, SanitizeURL(tt.args.ctx, tt.args.s), "sanitizeURL(%v, %v)", tt.args.ctx, tt.args.s)
|
||||
})
|
||||
}
|
||||
}
|
||||
174
pkg/middleware/loggermw/logger.go
Normal file
174
pkg/middleware/loggermw/logger.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 Unknwon
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package loggermw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Middleware() web.Middleware
|
||||
}
|
||||
|
||||
type loggerImpl struct {
|
||||
cfg *setting.Cfg
|
||||
flags featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func Provide(
|
||||
cfg *setting.Cfg,
|
||||
flags featuremgmt.FeatureToggles,
|
||||
) Logger {
|
||||
return &loggerImpl{
|
||||
cfg: cfg,
|
||||
flags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loggerImpl) Middleware() web.Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// we have to init the context with the counter here to update the request
|
||||
r = r.WithContext(log.InitCounter(r.Context()))
|
||||
// put the start time on context so we can measure it later.
|
||||
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
|
||||
|
||||
if l.flags.IsEnabled(featuremgmt.FlagUnifiedRequestLog) {
|
||||
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
|
||||
}
|
||||
|
||||
rw := web.Rw(w, r)
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
duration := time.Since(start)
|
||||
timeTaken := duration / time.Millisecond
|
||||
ctx := contexthandler.FromContext(r.Context())
|
||||
if ctx != nil && ctx.PerfmonTimer != nil {
|
||||
ctx.PerfmonTimer.Observe(float64(timeTaken))
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
logParams, logger := l.prepareLogParams(ctx, duration)
|
||||
logger.LogFunc(ctx.Logger)("Request Completed", logParams...)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loggerImpl) prepareLogParams(c *contextmodel.ReqContext, duration time.Duration) ([]any, errutil.LogLevel) {
|
||||
rw := c.Resp
|
||||
r := c.Req
|
||||
|
||||
status := rw.Status()
|
||||
lvl := errutil.LevelInfo
|
||||
|
||||
switch {
|
||||
case status == http.StatusOK, status == http.StatusNotModified:
|
||||
if !l.cfg.RouterLogging {
|
||||
lvl = errutil.LevelNever
|
||||
}
|
||||
case status >= http.StatusInternalServerError:
|
||||
lvl = errutil.LevelError
|
||||
}
|
||||
|
||||
logParams := []any{
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", status,
|
||||
"remote_addr", c.RemoteAddr(),
|
||||
"time_ms", int64(duration / time.Millisecond),
|
||||
"duration", duration.String(),
|
||||
"size", rw.Size(),
|
||||
}
|
||||
|
||||
referer, err := SanitizeURL(r.Referer())
|
||||
// We add an empty referer when there's a parsing error, hence this is before the err check.
|
||||
logParams = append(logParams, "referer", referer)
|
||||
if err != nil {
|
||||
logParams = append(logParams, "refererParsingErr", fmt.Errorf("received invalid referer in request headers, removed for log forgery prevention: %w", err))
|
||||
lvl = lvl.HighestOf(errutil.LevelWarn)
|
||||
}
|
||||
|
||||
if l.flags.IsEnabled(featuremgmt.FlagDatabaseMetrics) {
|
||||
logParams = append(logParams, "db_call_count", log.TotalDBCallCount(c.Req.Context()))
|
||||
}
|
||||
|
||||
if handler, exist := middleware.RouteOperationName(c.Req); exist {
|
||||
logParams = append(logParams, "handler", handler)
|
||||
}
|
||||
|
||||
logParams = append(logParams, errorLogParams(c.Error)...)
|
||||
|
||||
return logParams, lvl
|
||||
}
|
||||
|
||||
func errorLogParams(err error) []any {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var gfErr errutil.Error
|
||||
if !errors.As(err, &gfErr) {
|
||||
return []any{"error", err.Error()}
|
||||
}
|
||||
|
||||
return []any{
|
||||
"errorReason", gfErr.Reason,
|
||||
"errorMessageID", gfErr.MessageID,
|
||||
"error", gfErr.LogMessage,
|
||||
}
|
||||
}
|
||||
|
||||
var sensitiveQueryStrings = [...]string{
|
||||
"auth_token",
|
||||
}
|
||||
|
||||
func SanitizeURL(s string) (string, error) {
|
||||
if s == "" {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sanitize URL")
|
||||
}
|
||||
|
||||
// strip out sensitive query strings
|
||||
values := u.Query()
|
||||
for _, query := range sensitiveQueryStrings {
|
||||
values.Del(query)
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
213
pkg/middleware/loggermw/logger_test.go
Normal file
213
pkg/middleware/loggermw/logger_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package loggermw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func Test_sanitizeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Receiving empty string should return it",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "Receiving valid URL string should return it parsed",
|
||||
input: "https://grafana.com/",
|
||||
want: "https://grafana.com/",
|
||||
},
|
||||
{
|
||||
name: "Receiving invalid URL string should return empty string",
|
||||
input: "this is not a valid URL",
|
||||
want: "",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url, err := SanitizeURL(tt.input)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equalf(t, tt.want, url, "SanitizeURL(%v)", tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_prepareLog(t *testing.T) {
|
||||
type opts struct {
|
||||
Features []any
|
||||
RouterLogging bool
|
||||
}
|
||||
|
||||
grafanaFlavoredErr := errutil.NewBase(errutil.StatusNotFound, "test.notFound").Errorf("got error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts opts
|
||||
req *http.Request
|
||||
response web.ResponseWriter
|
||||
duration time.Duration
|
||||
error error
|
||||
|
||||
expectFields map[string]any
|
||||
expectAbsence map[string]struct{}
|
||||
expectedLevel errutil.LogLevel
|
||||
}{
|
||||
{
|
||||
name: "base case",
|
||||
req: mustRequest(http.NewRequest(http.MethodGet, "/", nil)),
|
||||
response: mockResponseWriter{},
|
||||
|
||||
expectFields: map[string]any{
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"status": 0,
|
||||
"remote_addr": "",
|
||||
"time_ms": 0,
|
||||
"duration": "0s",
|
||||
"size": 0,
|
||||
"referer": "",
|
||||
},
|
||||
expectAbsence: map[string]struct{}{
|
||||
"error": {},
|
||||
"db_call_count": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "base case",
|
||||
req: mustRequest(http.NewRequest(http.MethodGet, "/", nil)),
|
||||
response: mockResponseWriter{},
|
||||
|
||||
expectFields: map[string]any{
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"status": 0,
|
||||
"remote_addr": "",
|
||||
"time_ms": 0,
|
||||
"duration": "0s",
|
||||
"size": 0,
|
||||
"referer": "",
|
||||
},
|
||||
expectAbsence: map[string]struct{}{
|
||||
"error": {},
|
||||
"db_call_count": {},
|
||||
},
|
||||
expectedLevel: errutil.LevelInfo,
|
||||
},
|
||||
{
|
||||
name: "regular Go error",
|
||||
req: mustRequest(http.NewRequest(http.MethodGet, "/", nil)),
|
||||
response: mockResponseWriter{
|
||||
status: http.StatusInternalServerError,
|
||||
},
|
||||
error: fmt.Errorf("got an error"),
|
||||
|
||||
expectFields: map[string]any{
|
||||
"status": http.StatusInternalServerError,
|
||||
"error": "got an error",
|
||||
},
|
||||
expectAbsence: map[string]struct{}{
|
||||
"errorReason": {},
|
||||
"errorMessageID": {},
|
||||
},
|
||||
expectedLevel: errutil.LevelError,
|
||||
},
|
||||
{
|
||||
name: "Grafana-style error",
|
||||
req: mustRequest(http.NewRequest(http.MethodGet, "/", nil)),
|
||||
response: mockResponseWriter{
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
error: grafanaFlavoredErr,
|
||||
expectFields: map[string]any{
|
||||
"status": http.StatusNotFound,
|
||||
"error": "got error",
|
||||
"errorReason": errutil.StatusNotFound,
|
||||
"errorMessageID": "test.notFound",
|
||||
},
|
||||
expectedLevel: errutil.LevelInfo,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RouterLogging = tc.opts.RouterLogging
|
||||
l := Provide(cfg, featuremgmt.WithFeatures(tc.opts.Features...))
|
||||
|
||||
service, ok := l.(*loggerImpl)
|
||||
require.Truef(t, ok, "expected service to be of type (*loggerImpl), got (%T)", l)
|
||||
|
||||
c := &contextmodel.ReqContext{
|
||||
Context: &web.Context{
|
||||
Req: tc.req,
|
||||
Resp: tc.response,
|
||||
},
|
||||
Error: tc.error,
|
||||
}
|
||||
|
||||
logs, level := service.prepareLogParams(c, tc.duration)
|
||||
|
||||
require.Zero(t, len(logs)%2, "Each key must have an accompanying value")
|
||||
kv := map[any]any{}
|
||||
for i := 0; i < len(logs); i += 2 {
|
||||
kv[logs[i]] = logs[i+1]
|
||||
}
|
||||
|
||||
for key, val := range tc.expectFields {
|
||||
assert.Contains(t, kv, key)
|
||||
if val != nil {
|
||||
assert.EqualValues(t, val, kv[key])
|
||||
}
|
||||
}
|
||||
for key := range tc.expectAbsence {
|
||||
assert.NotContains(t, kv, key)
|
||||
}
|
||||
|
||||
if tc.expectedLevel != "" {
|
||||
assert.Equal(t, tc.expectedLevel, level)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustRequest(r *http.Request, err error) *http.Request {
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("expected no error when creating request, got: %w", err))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type mockResponseWriter struct {
|
||||
web.ResponseWriter
|
||||
|
||||
status int
|
||||
size int
|
||||
}
|
||||
|
||||
func (m mockResponseWriter) Status() int {
|
||||
return m.status
|
||||
}
|
||||
func (m mockResponseWriter) Size() int {
|
||||
return m.size
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Middleware {
|
||||
|
||||
handler := "unknown"
|
||||
// TODO: do not depend on web.Context from the future
|
||||
if routeOperation, exists := routeOperationName(web.FromContext(r.Context()).Req); exists {
|
||||
if routeOperation, exists := RouteOperationName(web.FromContext(r.Context()).Req); exists {
|
||||
handler = routeOperation
|
||||
} else {
|
||||
// if grafana does not recognize the handler and returns 404 we should register it as `notfound`
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestCanGetRouteNameFromContext(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
handler, _ := routeOperationName(req)
|
||||
handler, _ := RouteOperationName(req)
|
||||
assert.Equal(t, tc.expected, handler)
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,13 @@ func TestOperationNameCanOnlyBeSetOnce(t *testing.T) {
|
||||
req = addRouteNameToContext(req, "first")
|
||||
|
||||
// check that the operation name is set correctly
|
||||
value, exists := routeOperationName(req)
|
||||
value, exists := RouteOperationName(req)
|
||||
assert.True(t, exists, "route name should exist")
|
||||
assert.Equal(t, "first", value)
|
||||
|
||||
// check that it cannot be overwritten
|
||||
req = addRouteNameToContext(req, "second")
|
||||
value, exists = routeOperationName(req)
|
||||
value, exists = RouteOperationName(req)
|
||||
assert.True(t, exists, "route name should exist")
|
||||
assert.Equal(t, "first", value)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func ProvideRouteOperationName(name string) web.Handler {
|
||||
|
||||
func addRouteNameToContext(req *http.Request, operationName string) *http.Request {
|
||||
// don't set route name if it's set
|
||||
if _, exists := routeOperationName(req); exists {
|
||||
if _, exists := RouteOperationName(req); exists {
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ var unnamedHandlers = []struct {
|
||||
{handler: "/debug/pprof-handlers", pathPattern: regexp.MustCompile("^/debug/pprof")},
|
||||
}
|
||||
|
||||
// routeOperationName receives the route operation name from context, if set.
|
||||
func routeOperationName(req *http.Request) (string, bool) {
|
||||
// RouteOperationName receives the route operation name from context, if set.
|
||||
func RouteOperationName(req *http.Request) (string, bool) {
|
||||
if val := req.Context().Value(routeOperationNameKey); val != nil {
|
||||
op, ok := val.(string)
|
||||
return op, ok
|
||||
@@ -90,7 +90,7 @@ func RequestTracing(tracer tracing.Tracer) web.Middleware {
|
||||
// Only call span.Finish when a route operation name have been set,
|
||||
// meaning that not set the span would not be reported.
|
||||
// TODO: do not depend on web.Context from the future
|
||||
if routeOperation, exists := routeOperationName(web.FromContext(req.Context()).Req); exists {
|
||||
if routeOperation, exists := RouteOperationName(web.FromContext(req.Context()).Req); exists {
|
||||
defer span.End()
|
||||
span.SetName(fmt.Sprintf("HTTP %s %s", req.Method, routeOperation))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
loginpkg "github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/middleware/csrf"
|
||||
"github.com/grafana/grafana/pkg/middleware/loggermw"
|
||||
"github.com/grafana/grafana/pkg/modules"
|
||||
pluginDashboards "github.com/grafana/grafana/pkg/plugins/manager/dashboards"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
@@ -358,6 +359,7 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(tag.Service), new(*tagimpl.Service)),
|
||||
authnimpl.ProvideService,
|
||||
supportbundlesimpl.ProvideService,
|
||||
loggermw.Provide,
|
||||
modules.WireSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type ReqContext struct {
|
||||
AllowAnonymous bool
|
||||
SkipCache bool
|
||||
Logger log.Logger
|
||||
Error error
|
||||
// RequestNonce is a cryptographic request identifier for use with Content Security Policy.
|
||||
RequestNonce string
|
||||
IsPublicDashboardView bool
|
||||
@@ -138,7 +139,11 @@ func (ctx *ReqContext) writeErrOrFallback(status int, message string, err error)
|
||||
}
|
||||
}
|
||||
|
||||
logger(logMessage, "error", err, "remote_addr", ctx.RemoteAddr(), "traceID", traceID)
|
||||
if errutil.HasUnifiedLogging(ctx.Req.Context()) {
|
||||
ctx.Error = err
|
||||
} else {
|
||||
logger(logMessage, "error", err, "remote_addr", ctx.RemoteAddr(), "traceID", traceID)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := data["message"]; !ok && message != "" {
|
||||
|
||||
@@ -479,5 +479,11 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaAlertingSquad,
|
||||
},
|
||||
{
|
||||
Name: "unifiedRequestLog",
|
||||
Description: "Writes error logs to the request logger",
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaBackendPlatformSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -71,3 +71,4 @@ prometheusDataplane,alpha,@grafana/observability-metrics,false,false,false,false
|
||||
alertStateHistoryLokiSecondary,alpha,@grafana/alerting-squad,false,false,false,false
|
||||
alertStateHistoryLokiPrimary,alpha,@grafana/alerting-squad,false,false,false,false
|
||||
alertStateHistoryLokiOnly,alpha,@grafana/alerting-squad,false,false,false,false
|
||||
unifiedRequestLog,alpha,@grafana/backend-platform,false,false,false,false
|
||||
|
||||
|
@@ -294,4 +294,8 @@ const (
|
||||
// FlagAlertStateHistoryLokiOnly
|
||||
// Disable Grafana alerts from emitting annotations when a remote Loki instance is available.
|
||||
FlagAlertStateHistoryLokiOnly = "alertStateHistoryLokiOnly"
|
||||
|
||||
// FlagUnifiedRequestLog
|
||||
// Writes error logs to the request logger
|
||||
FlagUnifiedRequestLog = "unifiedRequestLog"
|
||||
)
|
||||
|
||||
@@ -565,7 +565,7 @@ func (g *GrafanaLive) handleOnRPC(client *centrifuge.Client, e centrifuge.RPCEve
|
||||
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
|
||||
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusForbidden), Message: http.StatusText(http.StatusForbidden)}
|
||||
}
|
||||
var gfErr *errutil.Error
|
||||
var gfErr errutil.Error
|
||||
if errors.As(err, &gfErr) && gfErr.Reason.Status() == errutil.StatusBadRequest {
|
||||
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusBadRequest), Message: http.StatusText(http.StatusBadRequest)}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package errutil
|
||||
|
||||
import "context"
|
||||
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
@@ -35,3 +37,36 @@ func (l LogLevel) LogFunc(logger LogInterface) func(msg string, ctx ...interface
|
||||
return logger.Error
|
||||
}
|
||||
}
|
||||
|
||||
func (l LogLevel) HighestOf(other LogLevel) LogLevel {
|
||||
if l.order() < other.order() {
|
||||
return other
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l LogLevel) order() int {
|
||||
switch l {
|
||||
case LevelNever:
|
||||
return 0
|
||||
case LevelDebug:
|
||||
return 1
|
||||
case LevelInfo:
|
||||
return 2
|
||||
case LevelWarn:
|
||||
return 3
|
||||
default: // LevelUnknown and LevelError.
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
type useUnifiedLogging struct{}
|
||||
|
||||
func SetUnifiedLogging(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, useUnifiedLogging{}, true)
|
||||
}
|
||||
|
||||
func HasUnifiedLogging(ctx context.Context) bool {
|
||||
v, ok := ctx.Value(useUnifiedLogging{}).(bool)
|
||||
return ok && v
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
|
||||
@@ -104,6 +106,9 @@ func (ctx *Context) HTML(status int, name string, data interface{}) {
|
||||
ctx.Resp.Header().Set(headerContentType, contentTypeHTML)
|
||||
ctx.Resp.WriteHeader(status)
|
||||
if err := ctx.template.ExecuteTemplate(ctx.Resp, name, data); err != nil {
|
||||
if errors.Is(err, syscall.EPIPE) { // Client has stopped listening.
|
||||
return
|
||||
}
|
||||
panic(fmt.Sprintf("Context.HTML - Error rendering template: %s. You may need to build frontend assets \n %s", name, err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user