Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-2539] added access/transaction logs #6354

Merged
merged 34 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a195b29
Add transactions logs for all API calls.
sedkis May 27, 2024
fae936a
updated config options and json formatter to access logs function
Jun 7, 2024
e5a8f1a
updated the logic for the handler error access logs
Jun 10, 2024
916fe84
updated the transaction log to use the JSON formatter
Jun 12, 2024
f4f4602
added transaction logs for error handler
Jun 12, 2024
fd4432f
removed the log format feature
Jun 13, 2024
928ab07
refactored the transaction log functionality
Jun 13, 2024
49530a0
add base tests funcs for log transaction
Jun 17, 2024
4dc7946
added benchmarks for both success and error handler with access logs
Jun 18, 2024
a4dd96a
ran linter script to clean up files
Jun 18, 2024
5b8565d
remove comment
Jun 18, 2024
436b3cc
refactored accesss logs to it's own object
Jun 18, 2024
49731f8
refactored the handle error to utilize the access logs obj
Jun 18, 2024
d886dc5
clean up access logs test file
Jun 18, 2024
5e1d338
decoupled and cleaned up the access logs code
Jun 19, 2024
6f6b672
fix merge conflict
Jun 19, 2024
1f7ef18
refactored the access log library and added unit testing
Jul 25, 2024
779cb14
added comments and cleaned up some code
Aug 1, 2024
44cf22d
updated the access log object and cleaned up some code
Aug 14, 2024
aa60b76
rename struct for consistency
Aug 14, 2024
698e9fb
Merge branch 'master' into TT-2539/transaction_logs
LLe27 Aug 29, 2024
f5afa32
updated the logic to reconstruct the target upstream URL
Sep 4, 2024
b4c94c7
remove coprocess files and added the gitignore extension
Sep 4, 2024
54cbffa
update the gitignore file to negate go-e files
Sep 5, 2024
88b336a
refactor recordAccessLog to be a receiver method on BaseMiddleware
jeffy-mathew Sep 9, 2024
ad9a5e3
merge master
jeffy-mathew Sep 9, 2024
0b28bbf
Merge branch 'master' into TT-2539/transaction_logs
jeffy-mathew Sep 10, 2024
3411591
Fix make lint
Sep 10, 2024
ab13fa6
Move token/hash functions from storage to internal/crypto
Sep 10, 2024
82f8e7c
Put access log in httputil submodule
Sep 10, 2024
5beee57
Update recordAccessLog usage, parameter order
Sep 10, 2024
72c2a42
Fix gofmt
Sep 10, 2024
b1dc70c
Merge branch 'master' into TT-2539/transaction_logs
jeffy-mathew Sep 12, 2024
6bb6182
revert certificates config upstream docs changes
jeffy-mathew Sep 12, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ session_state_gen_test.go
__pycache__/
tyk.test
tyk-gateway.pid
*.go-e
titpetric marked this conversation as resolved.
Show resolved Hide resolved

tyk_linux_*
/dist/
Expand Down
9 changes: 9 additions & 0 deletions cli/linter/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,15 @@
"type": "string",
"enum": ["", "debug", "info", "warn", "error"]
},
"access_logs": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"enable_http_profiler": {
"type": "boolean"
},
Expand Down
10 changes: 10 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ type AnalyticsConfigConfig struct {
SerializerType string `json:"serializer_type"`
}

// AccessLogsConfig defines the type of transactions logs printed to stdout
type AccessLogsConfig struct {
// Enable the transaction logs. Default: false
Enabled bool `json:"enabled"`
}

type HealthCheckConfig struct {
// Setting this value to `true` will enable the health-check endpoint on /Tyk/health.
EnableHealthChecks bool `json:"enable_health_checks"`
Expand Down Expand Up @@ -953,6 +959,10 @@ type Config struct {
// If not set or left empty, it will default to `info`.
LogLevel string `json:"log_level"`

// You can configure the transaction logs to be turned on
// If not set or left empty, it will default to 'false'
AccessLogs AccessLogsConfig `json:"access_logs"`

// Section for configuring OpenTracing support
// Deprecated: use OpenTelemetry instead.
Tracer Tracer `json:"tracing"`
Expand Down
31 changes: 31 additions & 0 deletions gateway/handler_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"strings"
"time"

"github.com/sirupsen/logrus"

"github.com/TykTechnologies/tyk/internal/httputil"
"github.com/TykTechnologies/tyk/storage"

"github.com/TykTechnologies/tyk/apidef"

"github.com/TykTechnologies/tyk-pump/analytics"
Expand Down Expand Up @@ -87,6 +92,25 @@ type TemplateExecutor interface {
Execute(wr io.Writer, data interface{}) error
}

func (e *ErrorHandler) recordAccessLog(log *logrus.Logger, req *http.Request, resp *http.Response) {
titpetric marked this conversation as resolved.
Show resolved Hide resolved
// Don't print the full token, handle as obfuscated key or hashed key for security reasons
hashKeys := e.Gw.GetConfig().HashKeys
token := ctxGetAuthToken(req)

if !hashKeys {
token = e.Gw.obfuscateKey(token)
} else {
token = storage.HashKey(token, hashKeys)
}

accessLog := httputil.NewAccessLogRecord(e.Spec.APIID, token, e.Spec.OrgID)
accessLog.WithLatency(&analytics.Latency{})
accessLog.WithRequest(req)
accessLog.WithResponse(resp)

log.WithFields(accessLog.Fields()).Info()
Fixed Show fixed Hide fixed
}

// HandleError is the actual error handler and will store the error details in analytics if analytics processing is enabled.
func (e *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, errMsg string, errCode int, writeResponse bool) {
defer e.Base().UpdateRequestSession(r)
Expand Down Expand Up @@ -311,6 +335,13 @@ func (e *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, errMs
log.WithError(err).Error("could not store analytic record")
}
}

LLe27 marked this conversation as resolved.
Show resolved Hide resolved
// Print the transaction logs for error situations if enabled. Success transaction
// logs will be handled by the "handler_success.go"
if e.Spec.GlobalConfig.AccessLogs.Enabled {
e.recordAccessLog(log, r, response)
}

// Report in health check
reportHealthValue(e.Spec, BlockedRequestLog, "-1")
}
51 changes: 51 additions & 0 deletions gateway/handler_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"strings"
"testing"

"github.com/TykTechnologies/tyk/config"

"github.com/TykTechnologies/tyk/header"
"github.com/TykTechnologies/tyk/test"
)
Expand Down Expand Up @@ -122,3 +124,52 @@ func TestHandleDefaultErrorJSON(t *testing.T) {
})

}

func BenchmarkErrorLogTransaction(b *testing.B) {
b.Run("AccessLogs enabled with Hashkeys set to true", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = true
globalConf.AccessLogs.Enabled = true
}
benchmarkErrorLogTransaction(b, conf)

})
b.Run("AccessLogs enabled with Hashkeys set to false", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = false
globalConf.AccessLogs.Enabled = true
}
benchmarkErrorLogTransaction(b, conf)
})

b.Run("AccessLogs disabled with Hashkeys set to true", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = true
globalConf.AccessLogs.Enabled = false
}
benchmarkErrorLogTransaction(b, conf)
})

b.Run("AccessLogs disabled with Hashkeys set to false", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = false
globalConf.AccessLogs.Enabled = false
}
benchmarkErrorLogTransaction(b, conf)
})
}

func benchmarkErrorLogTransaction(b *testing.B, conf func(globalConf *config.Config)) {
b.ReportAllocs()
b.Helper()
b.ResetTimer()

ts := StartTest(conf)
defer ts.Close()

for i := 0; i < b.N; i++ {
ts.Run(b, test.TestCase{
Code: http.StatusNotFound,
})
}
}
29 changes: 29 additions & 0 deletions gateway/handler_success.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
"strings"
"time"

"github.com/sirupsen/logrus"

graphqlinternal "github.com/TykTechnologies/tyk/internal/graphql"
"github.com/TykTechnologies/tyk/storage"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/internal/httputil"
Expand Down Expand Up @@ -120,6 +123,25 @@ func getSessionTags(session *user.SessionState) []string {
return tags
}

func (s *SuccessHandler) recordAccessLog(latency analytics.Latency, log *logrus.Logger, req *http.Request, resp *http.Response) {
// Don't print the full token, handle as obfuscated key or hashed key for security reasons
hashKeys := s.Gw.GetConfig().HashKeys
token := ctxGetAuthToken(req)

if !hashKeys {
token = s.Gw.obfuscateKey(token)
} else {
token = storage.HashKey(token, hashKeys)
}

accessLog := httputil.NewAccessLogRecord(s.Spec.APIID, token, s.Spec.OrgID)
accessLog.WithLatency(&latency)
accessLog.WithRequest(req)
accessLog.WithResponse(resp)

log.WithFields(accessLog.Fields()).Info()
Fixed Show fixed Hide fixed
}

func recordGraphDetails(rec *analytics.AnalyticsRecord, r *http.Request, resp *http.Response, spec *APISpec) {
if !spec.GraphQL.Enabled || spec.GraphQL.ExecutionMode == apidef.GraphQLExecutionModeSubgraph {
return
Expand Down Expand Up @@ -382,8 +404,15 @@ func (s *SuccessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) *http
Upstream: int64(DurationToMillisecond(resp.UpstreamLatency)),
}
s.RecordHit(r, latency, resp.Response.StatusCode, resp.Response, false)

// Don't print a transaction log there is no "resp", that indicates an error.
// In error situations, transaction log is already printed by "handler_error.go"
if s.Spec.GlobalConfig.AccessLogs.Enabled {
s.recordAccessLog(latency, log, r, resp.Response)
}
}
log.Debug("Done proxy")

return nil
}

Expand Down
55 changes: 55 additions & 0 deletions gateway/handler_success_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,58 @@ func TestAnalyticsIgnoreSubgraph(t *testing.T) {
)
assert.NoError(t, err)
}

func BenchmarkSuccessLogTransaction(b *testing.B) {
b.Run("AccessLogs enabled with Hashkeys set to true", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = true
globalConf.AccessLogs.Enabled = true
}
benchmarkSuccessLogTransaction(b, conf)

})
b.Run("AccessLogs enabled with Hashkeys set to false", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = false
globalConf.AccessLogs.Enabled = true
}
benchmarkSuccessLogTransaction(b, conf)
})
b.Run("AccessLogs disabled with Hashkeys set to true", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = true
globalConf.AccessLogs.Enabled = false
}
benchmarkSuccessLogTransaction(b, conf)
})
b.Run("AccessLogs disabled with Hashkeys set to false", func(b *testing.B) {
conf := func(globalConf *config.Config) {
globalConf.HashKeys = false
globalConf.AccessLogs.Enabled = false
}
benchmarkSuccessLogTransaction(b, conf)
})
}

func benchmarkSuccessLogTransaction(b *testing.B, conf func(globalConf *config.Config)) {
b.ReportAllocs()
LLe27 marked this conversation as resolved.
Show resolved Hide resolved
b.Helper()
b.ResetTimer()

ts := StartTest(conf)
defer ts.Close()

API := BuildAPI(func(spec *APISpec) {
spec.Name = "test-api"
spec.APIID = "test-api-id"
spec.Proxy.ListenPath = "/"
})[0]

ts.Gw.LoadAPI(API)

for i := 0; i < b.N; i++ {
ts.Run(b, test.TestCase{
Code: http.StatusOK,
})
}
}
71 changes: 71 additions & 0 deletions internal/httputil/access_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package httputil

import (
"net/http"
"net/url"

"github.com/TykTechnologies/tyk/request"

"github.com/sirupsen/logrus"

"github.com/TykTechnologies/tyk-pump/analytics"
)

// AccessLogRecord is a representation of a transaction log in the Gateway
type AccessLogRecord logrus.Fields

// NewAccessLogRecord returns an AccessLogRecord object
func NewAccessLogRecord(apiID string, apiKey string, orgId string) *AccessLogRecord {
a := &AccessLogRecord{
"APIID": apiID,
"APIKey": apiKey,
"OrgID": orgId,
"prefix": "access-log",
}

return a
}

// WithLatency sets the latency of the AccessLogRecord
func (a *AccessLogRecord) WithLatency(latency *analytics.Latency) *AccessLogRecord {
if latency != nil {
(*a)["TotalLatency"] = latency.Total
(*a)["UpstreamLatency"] = latency.Upstream
}
return a
}

// WithRequest sets the request of the AccessLogRecord
func (a *AccessLogRecord) WithRequest(req *http.Request) *AccessLogRecord {
if req != nil {
(*a)["ClientIP"] = request.RealIP(req)
(*a)["ClientRemoteAddr"] = req.RemoteAddr
(*a)["Host"] = req.Host
(*a)["Method"] = req.Method
(*a)["Proto"] = req.Proto
(*a)["RequestURI"] = req.RequestURI
(*a)["UpstreamAddress"] = (&url.URL{
Scheme: req.URL.Scheme,
Host: req.URL.Host,
Path: req.URL.Path,
RawQuery: req.URL.RawQuery,
}).String()
(*a)["UpstreamPath"] = req.URL.Path
(*a)["UpstreamURI"] = req.URL.RequestURI()
(*a)["UserAgent"] = req.UserAgent()
}
return a
}

// WithResponse sets the request of the AccessLogRecord
func (a *AccessLogRecord) WithResponse(resp *http.Response) *AccessLogRecord {
if resp != nil {
(*a)["StatusCode"] = resp.StatusCode
}
return a
}

// Fields converts the AccessLogRecord as a logrus.Fields object
func (a *AccessLogRecord) Fields() logrus.Fields {
return logrus.Fields(*a)
}
Loading