Skip to content

Commit b9494d9

Browse files
authored
feat: cns logger v2 [1/2] (#3437)
* feat: add new v2 zap logger package to CNS Signed-off-by: Evan Baker <[email protected]> * update loggerv2 configs, add custom Unmarshalling Signed-off-by: Evan Baker <[email protected]> * add custom Duration type, tests, cores enabled by config Signed-off-by: Evan Baker <[email protected]> * add config normalization for logger v2 Signed-off-by: Evan Baker <[email protected]> * lints Signed-off-by: Evan Baker <[email protected]> --------- Signed-off-by: Evan Baker <[email protected]>
1 parent c697ff6 commit b9494d9

File tree

18 files changed

+602
-4
lines changed

18 files changed

+602
-4
lines changed

cns/logger/log.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
)
88

99
var (
10-
Log *CNSLogger
11-
aiMetadata string // this var is set at build time.
10+
Log *CNSLogger
11+
aiMetadata string // this var is set at build time.
12+
AppInsightsIKey = aiMetadata
1213
)
1314

1415
// todo: the functions below should be removed. CNSLogger should be injected where needed and not used from package level scope.

cns/logger/v2/config.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package logger
2+
3+
import (
4+
"encoding/json"
5+
6+
loggerv1 "github.com/Azure/azure-container-networking/cns/logger"
7+
"github.com/Azure/azure-container-networking/internal/time"
8+
"github.com/pkg/errors"
9+
"go.uber.org/zap/zapcore"
10+
)
11+
12+
//nolint:unused // will be used
13+
const (
14+
defaultMaxBackups = 10
15+
defaultMaxSize = 10 // MB
16+
defaultMaxBatchInterval = 30 * time.Second
17+
defaultMaxBatchSize = 32000
18+
defaultGracePeriod = 30 * time.Second
19+
)
20+
21+
//nolint:unused // will be used
22+
var defaultIKey = loggerv1.AppInsightsIKey
23+
24+
// UnmarshalJSON implements json.Unmarshaler for the Config.
25+
// It only differs from the default by parsing the
26+
// Level string into a zapcore.Level and setting the level field.
27+
func (c *Config) UnmarshalJSON(data []byte) error {
28+
type Alias Config
29+
aux := &struct {
30+
*Alias
31+
}{
32+
Alias: (*Alias)(c),
33+
}
34+
if err := json.Unmarshal(data, &aux); err != nil { //nolint:musttag // doesn't understand the embedding strategy
35+
return errors.Wrap(err, "failed to unmarshal Config")
36+
}
37+
lvl, err := zapcore.ParseLevel(c.Level)
38+
if err != nil {
39+
return errors.Wrap(err, "failed to parse Config Level")
40+
}
41+
c.level = lvl
42+
return nil
43+
}
44+
45+
// Normalize checks the Config for missing or illegal values and sets them
46+
// to defaults if appropriate.
47+
func (c *Config) Normalize() {
48+
if c.File != nil {
49+
if c.File.Filepath == "" {
50+
c.File.Filepath = defaultFilePath
51+
}
52+
if c.File.MaxBackups == 0 {
53+
c.File.MaxBackups = defaultMaxBackups
54+
}
55+
if c.File.MaxSize == 0 {
56+
c.File.MaxSize = defaultMaxSize
57+
}
58+
}
59+
if c.AppInsights != nil {
60+
if c.AppInsights.IKey == "" {
61+
c.AppInsights.IKey = defaultIKey
62+
}
63+
if c.AppInsights.GracePeriod.Duration == 0 {
64+
c.AppInsights.GracePeriod.Duration = defaultGracePeriod
65+
}
66+
if c.AppInsights.MaxBatchInterval.Duration == 0 {
67+
c.AppInsights.MaxBatchInterval.Duration = defaultMaxBatchInterval
68+
}
69+
if c.AppInsights.MaxBatchSize == 0 {
70+
c.AppInsights.MaxBatchSize = defaultMaxBatchSize
71+
}
72+
}
73+
c.normalize()
74+
}

cns/logger/v2/config_linux.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package logger
2+
3+
import (
4+
cores "github.com/Azure/azure-container-networking/cns/logger/v2/cores"
5+
"go.uber.org/zap/zapcore"
6+
)
7+
8+
const defaultFilePath = "/var/log/azure-cns.log"
9+
10+
type Config struct {
11+
// Level is the general logging Level. If cores have more specific config it will override this.
12+
Level string `json:"level"`
13+
level zapcore.Level `json:"-"`
14+
AppInsights *cores.AppInsightsConfig `json:"appInsights,omitempty"`
15+
File *cores.FileConfig `json:"file,omitempty"`
16+
}
17+
18+
func (c *Config) normalize() {}

cns/logger/v2/config_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package logger
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
cores "github.com/Azure/azure-container-networking/cns/logger/v2/cores"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestUnmarshalJSON(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
have []byte
15+
want *Config
16+
wantErr bool
17+
}{
18+
{
19+
name: "valid",
20+
have: []byte(`{"level":"info"}`),
21+
want: &Config{
22+
Level: "info",
23+
level: 0,
24+
},
25+
},
26+
{
27+
name: "invalid level",
28+
have: []byte(`{"level":"invalid"}`),
29+
wantErr: true,
30+
},
31+
{
32+
name: "valid with file",
33+
have: []byte(`{"level":"info","file":{"filepath":"/k/azurecns/azure-cns.log"}}`),
34+
want: &Config{
35+
Level: "info",
36+
level: 0,
37+
File: &cores.FileConfig{
38+
Filepath: "/k/azurecns/azure-cns.log",
39+
},
40+
},
41+
},
42+
}
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
c := &Config{}
46+
err := json.Unmarshal(tt.have, c)
47+
if tt.wantErr {
48+
require.Error(t, err)
49+
return
50+
}
51+
require.NoError(t, err)
52+
require.Equal(t, tt.want, c)
53+
})
54+
}
55+
}

cns/logger/v2/config_windows.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package logger
2+
3+
import (
4+
cores "github.com/Azure/azure-container-networking/cns/logger/v2/cores"
5+
"go.uber.org/zap/zapcore"
6+
)
7+
8+
const defaultFilePath = "/k/azurecns/azure-cns.log"
9+
10+
type Config struct {
11+
// Level is the general logging Level. If cores have more specific config it will override this.
12+
Level string `json:"level"`
13+
level zapcore.Level `json:"-"`
14+
AppInsights *cores.AppInsightsConfig `json:"appInsights,omitempty"`
15+
File *cores.FileConfig `json:"file,omitempty"`
16+
ETW *cores.ETWConfig `json:"etw,omitempty"`
17+
}
18+
19+
func (c *Config) normalize() {
20+
if c.ETW != nil {
21+
if c.ETW.EventName == "" {
22+
c.ETW.EventName = "AzureCNS"
23+
}
24+
if c.ETW.ProviderName == "" {
25+
c.ETW.ProviderName = "ACN-Monitoring"
26+
}
27+
}
28+
}

cns/logger/v2/cores/ai.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package logger
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/Azure/azure-container-networking/internal/time"
7+
"github.com/Azure/azure-container-networking/zapai"
8+
"github.com/microsoft/ApplicationInsights-Go/appinsights"
9+
"github.com/pkg/errors"
10+
"go.uber.org/zap"
11+
"go.uber.org/zap/zapcore"
12+
)
13+
14+
type AppInsightsConfig struct {
15+
level zapcore.Level `json:"-"` // Zero value is default Info level.
16+
Level string `json:"level"`
17+
IKey string `json:"ikey"`
18+
GracePeriod time.Duration `json:"grace_period"`
19+
MaxBatchInterval time.Duration `json:"max_batch_interval"`
20+
MaxBatchSize int `json:"max_batch_size"`
21+
Fields []zapcore.Field `json:"fields"`
22+
}
23+
24+
// UnmarshalJSON implements json.Unmarshaler for the Config.
25+
// It only differs from the default by parsing the
26+
// Level string into a zapcore.Level and setting the level field.
27+
func (c *AppInsightsConfig) UnmarshalJSON(data []byte) error {
28+
type Alias AppInsightsConfig
29+
aux := &struct {
30+
*Alias
31+
}{
32+
Alias: (*Alias)(c),
33+
}
34+
if err := json.Unmarshal(data, &aux); err != nil {
35+
return errors.Wrap(err, "failed to unmarshal AppInsightsConfig")
36+
}
37+
lvl, err := zapcore.ParseLevel(c.Level)
38+
if err != nil {
39+
return errors.Wrap(err, "failed to parse AppInsightsConfig Level")
40+
}
41+
c.level = lvl
42+
return nil
43+
}
44+
45+
// ApplicationInsightsCore builds a zapcore.Core that sends logs to Application Insights.
46+
// The first return is the core, the second is a function to close the sink.
47+
func ApplicationInsightsCore(cfg *AppInsightsConfig) (zapcore.Core, func(), error) {
48+
// build the AI config
49+
aicfg := *appinsights.NewTelemetryConfiguration(cfg.IKey)
50+
aicfg.MaxBatchSize = cfg.MaxBatchSize
51+
aicfg.MaxBatchInterval = cfg.MaxBatchInterval.Duration
52+
sinkcfg := zapai.SinkConfig{
53+
GracePeriod: cfg.GracePeriod.Duration,
54+
TelemetryConfiguration: aicfg,
55+
}
56+
// open the AI zap sink
57+
sink, aiclose, err := zap.Open(sinkcfg.URI())
58+
if err != nil {
59+
return nil, aiclose, errors.Wrap(err, "failed to open AI sink")
60+
}
61+
// build the AI core
62+
core := zapai.NewCore(cfg.level, sink)
63+
core = core.WithFieldMappers(zapai.DefaultMappers)
64+
// add normalized fields for the built-in AI Tags
65+
// TODO(rbtr): move to the caller
66+
return core.With(cfg.Fields), aiclose, nil
67+
}

cns/logger/v2/cores/ai_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package logger
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/Azure/azure-container-networking/internal/time"
8+
"github.com/stretchr/testify/require"
9+
"go.uber.org/zap/zapcore"
10+
)
11+
12+
func TestAIConfigUnmarshalJSON(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
have []byte
16+
want *AppInsightsConfig
17+
wantErr bool
18+
}{
19+
{
20+
name: "valid",
21+
have: []byte(`{"grace_period":"30s","level":"panic","max_batch_interval":"30s","max_batch_size":32000}`),
22+
want: &AppInsightsConfig{
23+
GracePeriod: time.Duration{Duration: 30 * time.Second},
24+
Level: "panic",
25+
level: zapcore.PanicLevel,
26+
MaxBatchInterval: time.Duration{Duration: 30 * time.Second},
27+
MaxBatchSize: 32000,
28+
},
29+
},
30+
{
31+
name: "invalid level",
32+
have: []byte(`{"level":"invalid"}`),
33+
wantErr: true,
34+
},
35+
}
36+
for _, tt := range tests {
37+
t.Run(tt.name, func(t *testing.T) {
38+
c := &AppInsightsConfig{}
39+
err := json.Unmarshal(tt.have, c)
40+
if tt.wantErr {
41+
require.Error(t, err)
42+
return
43+
}
44+
require.NoError(t, err)
45+
require.Equal(t, tt.want, c)
46+
})
47+
}
48+
}

cns/logger/v2/cores/etw_windows.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package logger
2+
3+
import (
4+
"github.com/Azure/azure-container-networking/zapetw"
5+
"go.uber.org/zap"
6+
"go.uber.org/zap/zapcore"
7+
)
8+
9+
type ETWConfig struct {
10+
EventName string
11+
Level zapcore.Level
12+
ProviderName string
13+
}
14+
15+
// ETWCore builds a zapcore.Core that sends logs to ETW.
16+
// The first return is the core, the second is a function to close the sink.
17+
func ETWCore(cfg *ETWConfig) (zapcore.Core, func(), error) {
18+
encoderConfig := zap.NewProductionEncoderConfig()
19+
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
20+
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
21+
return zapetw.New(cfg.ProviderName, cfg.EventName, jsonEncoder, cfg.Level) //nolint:wrapcheck // ignore
22+
}

cns/logger/v2/cores/file.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package logger
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/pkg/errors"
7+
"go.uber.org/zap"
8+
"go.uber.org/zap/zapcore"
9+
"gopkg.in/natefinch/lumberjack.v2"
10+
)
11+
12+
type FileConfig struct {
13+
Filepath string `json:"filepath"`
14+
Level string `json:"level"`
15+
level zapcore.Level `json:"-"`
16+
MaxBackups int `json:"maxBackups"`
17+
MaxSize int `json:"maxSize"`
18+
}
19+
20+
// UnmarshalJSON implements json.Unmarshaler for the Config.
21+
// It only differs from the default by parsing the
22+
// Level string into a zapcore.Level and setting the level field.
23+
func (cfg *FileConfig) UnmarshalJSON(data []byte) error {
24+
type Alias FileConfig
25+
aux := &struct {
26+
*Alias
27+
}{
28+
Alias: (*Alias)(cfg),
29+
}
30+
if err := json.Unmarshal(data, &aux); err != nil {
31+
return errors.Wrap(err, "failed to unmarshal FileConfig")
32+
}
33+
lvl, err := zapcore.ParseLevel(cfg.Level)
34+
if err != nil {
35+
return errors.Wrap(err, "failed to parse FileConfig Level")
36+
}
37+
cfg.level = lvl
38+
return nil
39+
}
40+
41+
// FileCore builds a zapcore.Core that writes to a file.
42+
// The first return is the core, the second is a function to close the file.
43+
func FileCore(cfg *FileConfig) (zapcore.Core, func(), error) {
44+
filesink := &lumberjack.Logger{
45+
Filename: cfg.Filepath,
46+
MaxSize: cfg.MaxSize, // MB
47+
MaxBackups: cfg.MaxBackups,
48+
}
49+
encoderConfig := zap.NewProductionEncoderConfig()
50+
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
51+
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
52+
return zapcore.NewCore(jsonEncoder, zapcore.AddSync(filesink), cfg.level), func() { _ = filesink.Close() }, nil
53+
}

0 commit comments

Comments
 (0)