Skip to content

Commit 9f1c11b

Browse files
committed
apply logic offered by andy
1 parent d1e8342 commit 9f1c11b

12 files changed

Lines changed: 576 additions & 115 deletions

File tree

cli/linter/schema.json

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,36 @@
484484
"required": ["response"]
485485
}
486486
}
487+
},
488+
"SinkConfig": {
489+
"type": "object",
490+
"properties": {
491+
"level": {
492+
"type": "string",
493+
"description": "Specifies the minimum severity level (e.g., debug, info, warn) an event must have to be processed by this sink."
494+
},
495+
"high_level": {
496+
"type": "string",
497+
"description": "Specifies the maximum severity level (e.g., debug, info, warn). Default value: panic"
498+
},
499+
"format": {
500+
"type": "string",
501+
"description": "Defines the identifier of the used formatter (e.g., \"json\", \"text\")."
502+
},
503+
"format_options": {
504+
"type": "object",
505+
"description": "Raw JSON payload with parameters specific to the chosen Format."
506+
},
507+
"output": {
508+
"type": "string",
509+
"description": "Defines the identifier of the target destination (e.g., \"file\", \"http\", \"stdout\")."
510+
},
511+
"output_options": {
512+
"type": "object",
513+
"description": "Raw JSON payload with parameters specific to the chosen Output."
514+
}
515+
},
516+
"additionalProperties": false
487517
}
488518
},
489519
"properties": {
@@ -987,8 +1017,21 @@
9871017
"enum": ["", "debug", "info", "warn", "error"]
9881018
},
9891019
"log_format": {
990-
"type": "string",
991-
"enum": ["", "text", "json", "legacy"]
1020+
"oneOf": [
1021+
{
1022+
"type": "string",
1023+
"enum": ["", "text", "json", "legacy"]
1024+
},
1025+
{
1026+
"type": "array",
1027+
"items": {
1028+
"$ref": "#/definitions/SinkConfig"
1029+
}
1030+
},
1031+
{
1032+
"type": "null"
1033+
}
1034+
]
9921035
},
9931036
"access_logs": {
9941037
"type": ["object", "null"],

config/config.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,12 +1261,14 @@ type Config struct {
12611261

12621262
// You can now set a logging level (log_level). The following levels can be set: debug, info, warn, error.
12631263
// If not set or left empty, it will default to `info`.
1264+
// Is ignored when LogFormat is an array of sinks.
12641265
LogLevel logger.Level `json:"log_level"`
12651266

12661267
// LogFormat configures the output format of the logs.
1267-
// Allowed values are `text`, `json`, or `legacy`.
1268+
// Allowed values are null, `text`, `json`, `legacy` or array of sinks.
12681269
// If not set or left empty, it defaults to `text`.
1269-
LogFormat logger.Format `json:"log_format"`
1270+
// Sinks configures the output, format and level for each sink.
1271+
LogFormat LogFormat `json:"log_format"`
12701272

12711273
// AccessLogs configures the output for access logs.
12721274
// If not configured, the access log is disabled.

config/log_format.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
8+
tyklog "github.com/TykTechnologies/tyk/log"
9+
)
10+
11+
type LogFormat struct {
12+
sinks []tyklog.SinkConfig
13+
format tyklog.Format
14+
formatType LogFormatType
15+
}
16+
17+
func (o *LogFormat) Type() LogFormatType {
18+
return o.formatType
19+
}
20+
21+
func (o *LogFormat) Format() (tyklog.Format, bool) {
22+
return o.format, o.formatType == LogFormatString
23+
}
24+
25+
func (o *LogFormat) Sinks() ([]tyklog.SinkConfig, bool) {
26+
return o.sinks, o.formatType == LogFormatSinks
27+
}
28+
29+
func (o *LogFormat) UnmarshalJSON(data []byte) error {
30+
if string(data) == "null" || string(data) == `""` {
31+
return nil
32+
}
33+
34+
var errs []error
35+
36+
var legacyStr tyklog.Format
37+
if err := json.Unmarshal(data, &legacyStr); err == nil {
38+
if !legacyStr.Valid() {
39+
return fmt.Errorf("invalid format %q", string(legacyStr))
40+
}
41+
42+
o.formatType = LogFormatString
43+
o.format = legacyStr
44+
return nil
45+
} else {
46+
errs = append(errs, err)
47+
}
48+
49+
var sinks []tyklog.SinkConfig
50+
if err := json.Unmarshal(data, &sinks); err == nil {
51+
o.formatType = LogFormatSinks
52+
o.sinks = sinks
53+
return nil
54+
} else {
55+
errs = append(errs, err)
56+
}
57+
58+
return fmt.Errorf("invalid log_format: must be a string, null, or an array of tyklog.SinkConfig: %w", errors.Join(errs...))
59+
}
60+
61+
func (o LogFormat) MarshalJSON() ([]byte, error) {
62+
switch o.formatType {
63+
case LogFormatString:
64+
return json.Marshal(o.format)
65+
case LogFormatSinks:
66+
return json.Marshal(o.sinks)
67+
case LogFormatUndefined:
68+
fallthrough
69+
default:
70+
return []byte("null"), nil
71+
}
72+
}
73+
74+
type LogFormatType int
75+
76+
const (
77+
LogFormatUndefined LogFormatType = iota
78+
LogFormatString
79+
LogFormatSinks
80+
)

config/log_format_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
tyklog "github.com/TykTechnologies/tyk/log"
10+
)
11+
12+
func Test_Log_Format(t *testing.T) {
13+
type TestConfig struct {
14+
LogFormat LogFormat `json:"log_format"`
15+
}
16+
17+
t.Run("UnmarshalJSON", func(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
inputJSON string
21+
expectedStruct TestConfig
22+
expectError bool
23+
}{
24+
{
25+
name: "Valid string format provided",
26+
inputJSON: `{"log_format": "text"}`,
27+
expectedStruct: TestConfig{
28+
LogFormat: LogFormat{
29+
formatType: LogFormatString,
30+
format: tyklog.Format("text"),
31+
},
32+
},
33+
expectError: false,
34+
},
35+
{
36+
name: "Valid array of sinks provided",
37+
inputJSON: `{"log_format": [{}]}`,
38+
expectedStruct: TestConfig{
39+
LogFormat: LogFormat{
40+
formatType: LogFormatSinks,
41+
sinks: []tyklog.SinkConfig{{}},
42+
},
43+
},
44+
expectError: false,
45+
},
46+
{
47+
name: "Explicit null value provided",
48+
inputJSON: `{"log_format": null}`,
49+
expectedStruct: TestConfig{
50+
LogFormat: LogFormat{
51+
formatType: LogFormatUndefined,
52+
},
53+
},
54+
expectError: false,
55+
},
56+
{
57+
name: "Key is completely missing",
58+
inputJSON: `{}`,
59+
expectedStruct: TestConfig{
60+
LogFormat: LogFormat{
61+
formatType: LogFormatUndefined,
62+
},
63+
},
64+
expectError: false,
65+
},
66+
{
67+
name: "Invalid JSON structure (object instead of string/array)",
68+
inputJSON: `{"log_format": {"type": "stdout"}}`,
69+
expectError: true,
70+
},
71+
{
72+
name: "Invalid primitive type",
73+
inputJSON: `{"log_format": 12345}`,
74+
expectError: true,
75+
},
76+
{
77+
name: "Invalid string format (Valid() returns false)",
78+
inputJSON: `{"log_format": "garbage_format"}`,
79+
expectError: true,
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
var got TestConfig
86+
err := json.Unmarshal([]byte(tt.inputJSON), &got)
87+
88+
if tt.expectError {
89+
assert.Error(t, err, "Expected an unmarshal error")
90+
} else {
91+
assert.NoError(t, err, "Did not expect an unmarshal error")
92+
assert.Equal(t, tt.expectedStruct, got, "Parsed struct does not match expected state")
93+
}
94+
})
95+
}
96+
})
97+
98+
t.Run("MarshalJSON", func(t *testing.T) {
99+
tests := []struct {
100+
name string
101+
inputStruct TestConfig
102+
expectedJSON string
103+
}{
104+
{
105+
name: "LogFormatString state serializes to string",
106+
inputStruct: TestConfig{
107+
LogFormat: LogFormat{
108+
formatType: LogFormatString,
109+
format: tyklog.Format("text"),
110+
},
111+
},
112+
expectedJSON: `{"log_format": "text"}`,
113+
},
114+
{
115+
name: "LogFormatSinks state serializes to array",
116+
inputStruct: TestConfig{
117+
LogFormat: LogFormat{
118+
formatType: LogFormatSinks,
119+
sinks: []tyklog.SinkConfig{{}},
120+
},
121+
},
122+
expectedJSON: `{"log_format": [{}]}`,
123+
},
124+
{
125+
name: "LogFormatUndefined state forces null output",
126+
inputStruct: TestConfig{
127+
LogFormat: LogFormat{
128+
formatType: LogFormatUndefined,
129+
format: tyklog.Format("should_be_ignored"),
130+
},
131+
},
132+
expectedJSON: `{"log_format": null}`,
133+
},
134+
{
135+
name: "Empty initialized struct defaults to null",
136+
inputStruct: TestConfig{},
137+
expectedJSON: `{"log_format": null}`,
138+
},
139+
}
140+
141+
for _, tt := range tests {
142+
t.Run(tt.name, func(t *testing.T) {
143+
outBytes, err := json.Marshal(&tt.inputStruct)
144+
145+
assert.NoError(t, err, "Did not expect a marshal error")
146+
assert.JSONEq(t, tt.expectedJSON, string(outBytes), "Marshaled JSON does not match expected")
147+
})
148+
}
149+
})
150+
}

0 commit comments

Comments
 (0)