Skip to content

Commit

Permalink
feat: cns logger v2 [1/2] (#3437)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
rbtr authored Feb 27, 2025
1 parent c697ff6 commit b9494d9
Show file tree
Hide file tree
Showing 18 changed files with 602 additions and 4 deletions.
5 changes: 3 additions & 2 deletions cns/logger/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
)

var (
Log *CNSLogger
aiMetadata string // this var is set at build time.
Log *CNSLogger
aiMetadata string // this var is set at build time.
AppInsightsIKey = aiMetadata
)

// todo: the functions below should be removed. CNSLogger should be injected where needed and not used from package level scope.
Expand Down
74 changes: 74 additions & 0 deletions cns/logger/v2/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package logger

import (
"encoding/json"

loggerv1 "github.com/Azure/azure-container-networking/cns/logger"
"github.com/Azure/azure-container-networking/internal/time"
"github.com/pkg/errors"
"go.uber.org/zap/zapcore"
)

//nolint:unused // will be used
const (
defaultMaxBackups = 10
defaultMaxSize = 10 // MB
defaultMaxBatchInterval = 30 * time.Second
defaultMaxBatchSize = 32000
defaultGracePeriod = 30 * time.Second
)

//nolint:unused // will be used
var defaultIKey = loggerv1.AppInsightsIKey

// UnmarshalJSON implements json.Unmarshaler for the Config.
// It only differs from the default by parsing the
// Level string into a zapcore.Level and setting the level field.
func (c *Config) UnmarshalJSON(data []byte) error {
type Alias Config
aux := &struct {
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil { //nolint:musttag // doesn't understand the embedding strategy
return errors.Wrap(err, "failed to unmarshal Config")
}
lvl, err := zapcore.ParseLevel(c.Level)
if err != nil {
return errors.Wrap(err, "failed to parse Config Level")
}
c.level = lvl
return nil
}

// Normalize checks the Config for missing or illegal values and sets them
// to defaults if appropriate.
func (c *Config) Normalize() {
if c.File != nil {
if c.File.Filepath == "" {
c.File.Filepath = defaultFilePath
}
if c.File.MaxBackups == 0 {
c.File.MaxBackups = defaultMaxBackups
}
if c.File.MaxSize == 0 {
c.File.MaxSize = defaultMaxSize
}
}
if c.AppInsights != nil {
if c.AppInsights.IKey == "" {
c.AppInsights.IKey = defaultIKey
}
if c.AppInsights.GracePeriod.Duration == 0 {
c.AppInsights.GracePeriod.Duration = defaultGracePeriod
}
if c.AppInsights.MaxBatchInterval.Duration == 0 {
c.AppInsights.MaxBatchInterval.Duration = defaultMaxBatchInterval
}
if c.AppInsights.MaxBatchSize == 0 {
c.AppInsights.MaxBatchSize = defaultMaxBatchSize
}
}
c.normalize()
}
18 changes: 18 additions & 0 deletions cns/logger/v2/config_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package logger

import (
cores "github.com/Azure/azure-container-networking/cns/logger/v2/cores"
"go.uber.org/zap/zapcore"
)

const defaultFilePath = "/var/log/azure-cns.log"

type Config struct {
// Level is the general logging Level. If cores have more specific config it will override this.
Level string `json:"level"`
level zapcore.Level `json:"-"`
AppInsights *cores.AppInsightsConfig `json:"appInsights,omitempty"`
File *cores.FileConfig `json:"file,omitempty"`
}

func (c *Config) normalize() {}
55 changes: 55 additions & 0 deletions cns/logger/v2/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package logger

import (
"encoding/json"
"testing"

cores "github.com/Azure/azure-container-networking/cns/logger/v2/cores"
"github.com/stretchr/testify/require"
)

func TestUnmarshalJSON(t *testing.T) {
tests := []struct {
name string
have []byte
want *Config
wantErr bool
}{
{
name: "valid",
have: []byte(`{"level":"info"}`),
want: &Config{
Level: "info",
level: 0,
},
},
{
name: "invalid level",
have: []byte(`{"level":"invalid"}`),
wantErr: true,
},
{
name: "valid with file",
have: []byte(`{"level":"info","file":{"filepath":"/k/azurecns/azure-cns.log"}}`),
want: &Config{
Level: "info",
level: 0,
File: &cores.FileConfig{
Filepath: "/k/azurecns/azure-cns.log",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Config{}
err := json.Unmarshal(tt.have, c)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, c)
})
}
}
28 changes: 28 additions & 0 deletions cns/logger/v2/config_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package logger

import (
cores "github.com/Azure/azure-container-networking/cns/logger/v2/cores"
"go.uber.org/zap/zapcore"
)

const defaultFilePath = "/k/azurecns/azure-cns.log"

type Config struct {
// Level is the general logging Level. If cores have more specific config it will override this.
Level string `json:"level"`
level zapcore.Level `json:"-"`
AppInsights *cores.AppInsightsConfig `json:"appInsights,omitempty"`
File *cores.FileConfig `json:"file,omitempty"`
ETW *cores.ETWConfig `json:"etw,omitempty"`
}

func (c *Config) normalize() {
if c.ETW != nil {
if c.ETW.EventName == "" {
c.ETW.EventName = "AzureCNS"
}
if c.ETW.ProviderName == "" {
c.ETW.ProviderName = "ACN-Monitoring"
}
}
}
67 changes: 67 additions & 0 deletions cns/logger/v2/cores/ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package logger

import (
"encoding/json"

"github.com/Azure/azure-container-networking/internal/time"
"github.com/Azure/azure-container-networking/zapai"
"github.com/microsoft/ApplicationInsights-Go/appinsights"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type AppInsightsConfig struct {
level zapcore.Level `json:"-"` // Zero value is default Info level.
Level string `json:"level"`
IKey string `json:"ikey"`
GracePeriod time.Duration `json:"grace_period"`
MaxBatchInterval time.Duration `json:"max_batch_interval"`
MaxBatchSize int `json:"max_batch_size"`
Fields []zapcore.Field `json:"fields"`
}

// UnmarshalJSON implements json.Unmarshaler for the Config.
// It only differs from the default by parsing the
// Level string into a zapcore.Level and setting the level field.
func (c *AppInsightsConfig) UnmarshalJSON(data []byte) error {
type Alias AppInsightsConfig
aux := &struct {
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
return errors.Wrap(err, "failed to unmarshal AppInsightsConfig")
}
lvl, err := zapcore.ParseLevel(c.Level)
if err != nil {
return errors.Wrap(err, "failed to parse AppInsightsConfig Level")
}
c.level = lvl
return nil
}

// ApplicationInsightsCore builds a zapcore.Core that sends logs to Application Insights.
// The first return is the core, the second is a function to close the sink.
func ApplicationInsightsCore(cfg *AppInsightsConfig) (zapcore.Core, func(), error) {
// build the AI config
aicfg := *appinsights.NewTelemetryConfiguration(cfg.IKey)
aicfg.MaxBatchSize = cfg.MaxBatchSize
aicfg.MaxBatchInterval = cfg.MaxBatchInterval.Duration
sinkcfg := zapai.SinkConfig{
GracePeriod: cfg.GracePeriod.Duration,
TelemetryConfiguration: aicfg,
}
// open the AI zap sink
sink, aiclose, err := zap.Open(sinkcfg.URI())
if err != nil {
return nil, aiclose, errors.Wrap(err, "failed to open AI sink")
}
// build the AI core
core := zapai.NewCore(cfg.level, sink)
core = core.WithFieldMappers(zapai.DefaultMappers)
// add normalized fields for the built-in AI Tags
// TODO(rbtr): move to the caller
return core.With(cfg.Fields), aiclose, nil
}
48 changes: 48 additions & 0 deletions cns/logger/v2/cores/ai_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package logger

import (
"encoding/json"
"testing"

"github.com/Azure/azure-container-networking/internal/time"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"
)

func TestAIConfigUnmarshalJSON(t *testing.T) {
tests := []struct {
name string
have []byte
want *AppInsightsConfig
wantErr bool
}{
{
name: "valid",
have: []byte(`{"grace_period":"30s","level":"panic","max_batch_interval":"30s","max_batch_size":32000}`),
want: &AppInsightsConfig{
GracePeriod: time.Duration{Duration: 30 * time.Second},
Level: "panic",
level: zapcore.PanicLevel,
MaxBatchInterval: time.Duration{Duration: 30 * time.Second},
MaxBatchSize: 32000,
},
},
{
name: "invalid level",
have: []byte(`{"level":"invalid"}`),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &AppInsightsConfig{}
err := json.Unmarshal(tt.have, c)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, c)
})
}
}
22 changes: 22 additions & 0 deletions cns/logger/v2/cores/etw_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package logger

import (
"github.com/Azure/azure-container-networking/zapetw"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type ETWConfig struct {
EventName string
Level zapcore.Level
ProviderName string
}

// ETWCore builds a zapcore.Core that sends logs to ETW.
// The first return is the core, the second is a function to close the sink.
func ETWCore(cfg *ETWConfig) (zapcore.Core, func(), error) {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
return zapetw.New(cfg.ProviderName, cfg.EventName, jsonEncoder, cfg.Level) //nolint:wrapcheck // ignore
}
53 changes: 53 additions & 0 deletions cns/logger/v2/cores/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package logger

import (
"encoding/json"

"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)

type FileConfig struct {
Filepath string `json:"filepath"`
Level string `json:"level"`
level zapcore.Level `json:"-"`
MaxBackups int `json:"maxBackups"`
MaxSize int `json:"maxSize"`
}

// UnmarshalJSON implements json.Unmarshaler for the Config.
// It only differs from the default by parsing the
// Level string into a zapcore.Level and setting the level field.
func (cfg *FileConfig) UnmarshalJSON(data []byte) error {
type Alias FileConfig
aux := &struct {
*Alias
}{
Alias: (*Alias)(cfg),
}
if err := json.Unmarshal(data, &aux); err != nil {
return errors.Wrap(err, "failed to unmarshal FileConfig")
}
lvl, err := zapcore.ParseLevel(cfg.Level)
if err != nil {
return errors.Wrap(err, "failed to parse FileConfig Level")
}
cfg.level = lvl
return nil
}

// FileCore builds a zapcore.Core that writes to a file.
// The first return is the core, the second is a function to close the file.
func FileCore(cfg *FileConfig) (zapcore.Core, func(), error) {
filesink := &lumberjack.Logger{
Filename: cfg.Filepath,
MaxSize: cfg.MaxSize, // MB
MaxBackups: cfg.MaxBackups,
}
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
return zapcore.NewCore(jsonEncoder, zapcore.AddSync(filesink), cfg.level), func() { _ = filesink.Close() }, nil
}
Loading

0 comments on commit b9494d9

Please sign in to comment.