Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions internal/loggerfx/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

/*
Package loggerfx provides a function that can be used to create a logger module
for an fx application. The logger module is based on the sallust, goschtalt,
and zap libraries and provides a simple, tested way to include a standard logger.

Example Usage:

package main

import (
"github.com/goschtalt/goschtalt"
"github.com/xmidt-org/sallust"
"github.com/xmidt-org/skeleton/internal/loggerfx"
"go.uber.org/fx"
"go.uber.org/zap"
)

func main() {
cfg := struct {
Logging sallust.Config
}{
Logging: sallust.Config{
Level: "info",
OutputPaths: []string{},
ErrorOutputPaths: []string{},
},
}

config, err := goschtalt.New(
goschtalt.ConfigIs("two_words"),
goschtalt.AddValue("built-in", goschtalt.Root, cfg, goschtalt.AsDefault()),
)
if err != nil {
panic(err)
}

app := fx.New(
fx.Provide(
func() *goschtalt.Config {
return config
}),

loggerfx.Module(),
fx.Invoke(func(logger *zap.Logger) {
logger.Info("Hello, world!")
}),
loggerfx.SyncOnShutdown(),
)

app.Run()
}
*/
package loggerfx
122 changes: 122 additions & 0 deletions internal/loggerfx/loggerfx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package loggerfx

import (
"errors"
"fmt"

"github.com/goschtalt/goschtalt"
"github.com/xmidt-org/sallust"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

const (
deaultConfigPath = "logging"
)

var (
ErrInvalidConfigPath = errors.New("configuration structure path like 'logging' or 'foo.logging' is required")
)

// Module is function that builds the loggerfx module based on the inputs. If
// the configPath is not provided then the default path is used.
func Module(configPath ...string) fx.Option {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good pattern, but not something we want to import into every application. This is the kind of code that belongs in each application that uses these libraries.

The main reason for this is you cannot know a priori how an application is going to organize its modules. An application might put each http.Server in its own module, for example.

configPath = append(configPath, deaultConfigPath)

var path string
for _, cp := range configPath {
if cp != "" {
path = cp
break
}
}

// Why not use sallust.WithLogger()? It is because we want to provide the
// developer configuration based on the sallust configuration, not the zap
// options. This makes the configuration consistent between the two modes.
return fx.Options(
// Provide the logger configuration based on the input path.
fx.Provide(
goschtalt.UnmarshalFunc[sallust.Config](path),
provideLogger,
),

// Inform fx that we are providing a logger for it.
fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
return &fxevent.ZapLogger{Logger: log}
}),
)
}

// DefaultConfig is a helper function that creates a default sallust configuration
// for the logger based on the application name.
func DefaultConfig(appName string) sallust.Config {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good convention, but it should stay a convention. I'm ok with this being in skeleton, but we should express a dependency on this library via go.mod

return sallust.Config{
// Use the default zap logger configuration for most of the settings.
OutputPaths: []string{
fmt.Sprintf("/var/log/%s/%s.log", appName, appName),
},
ErrorOutputPaths: []string{
fmt.Sprintf("/var/log/%s/%s.log", appName, appName),
},
Rotation: &sallust.Rotation{
MaxSize: 50,
MaxBackups: 10,
MaxAge: 2,
},
}
}

// SyncOnShutdown is a helper function that returns an fx option that will
// sync the logger on shutdown.
//
// Make sure to include this option last in the fx.Options list, so that the
// logger is the last component to be shutdown.
func SyncOnShutdown() fx.Option {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what value this provides, since sallust provides the option.

return sallust.SyncOnShutdown()
}

// LoggerIn describes the dependencies used to bootstrap a zap logger within
// an fx application.
type LoggerIn struct {
fx.In

// Config is the sallust configuration for the logger. This component is optional,
// and if not supplied a default zap logger will be created.
Config sallust.Config `optional:"true"`

// DevMode is a flag that indicates if the logger should be in debug mode.
DevMode bool `name:"loggerfx.dev_mode" optional:"true"`
}

// Create the logger and configure it based on if the program is in
// debug mode or normal mode.
func provideLogger(in LoggerIn) (*zap.Logger, error) {
if in.DevMode {
in.Config.Level = "DEBUG"
in.Config.Development = true
in.Config.Encoding = "console"
in.Config.EncoderConfig = sallust.EncoderConfig{
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
FunctionKey: zapcore.OmitKey,
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: "capitalColor",
EncodeTime: "RFC3339",
EncodeDuration: "string",
EncodeCaller: "short",
}
in.Config.OutputPaths = []string{"stderr"}
in.Config.ErrorOutputPaths = []string{"stderr"}
}
return in.Config.Build()
}
162 changes: 162 additions & 0 deletions internal/loggerfx/loggerfx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package loggerfx

import (
"bytes"
"io"
"os"
"testing"

"github.com/goschtalt/goschtalt"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xmidt-org/sallust"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"go.uber.org/zap"
)

/*
func TestModule(t *testing.T) {
app := fxtest.New(
t,
Module(),
fx.Invoke(func(logger *zap.Logger) {
assert.NotNil(t, logger)
}),
)
defer app.RequireStart().RequireStop()
}
*/

func TestDefaultConfig(t *testing.T) {
appName := "testapp"
config := DefaultConfig(appName)

expectedConfig := sallust.Config{
OutputPaths: []string{
"/var/log/testapp/testapp.log",
},
ErrorOutputPaths: []string{
"/var/log/testapp/testapp.log",
},
Rotation: &sallust.Rotation{
MaxSize: 50,
MaxBackups: 10,
MaxAge: 2,
},
}

assert.Equal(t, expectedConfig, config)
}

func TestLoggerFX_EndToEnd(t *testing.T) {
// Create a temporary configuration file
loggerConfig := struct {
Logger sallust.Config
}{
Logger: sallust.Config{
Level: "info",
OutputPaths: []string{},
ErrorOutputPaths: []string{},
},
}
loggingConfig := struct {
Logging sallust.Config
}{
Logging: sallust.Config{
Level: "info",
OutputPaths: []string{},
ErrorOutputPaths: []string{},
},
}

tests := []struct {
name string
input any
label string
dev bool
err bool
}{
{
name: "empty path, no error",
input: loggingConfig,
}, {
name: "specific path, no error",
input: loggerConfig,
label: "logger",
}, {
name: "empty path, no error, dev mode",
input: loggingConfig,
dev: true,
}, {
name: "specific path, no error, dev mode",
input: loggerConfig,
label: "logger",
dev: true,
}, {
name: "specific path, error",
input: loggerConfig,
label: "invalid",
err: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Initialize goschtalt with the configuration file
config, err := goschtalt.New(
goschtalt.ConfigIs("two_words"),
goschtalt.AddValue("built-in", goschtalt.Root, test.input, goschtalt.AsDefault()),
)
require.NoError(t, err)

opt := Module()
if test.label != "" {
opt = Module(test.label)
}

// Capture stderr
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

var stderr bytes.Buffer
done := make(chan struct{})
go func() {
io.Copy(&stderr, r)
close(done)
}()

opts := []fx.Option{
fx.Supply(config),
fx.Supply(fx.Annotate(test.dev, fx.ResultTags(`name:"loggerfx.dev_mode"`))),
opt,
fx.Invoke(func(logger *zap.Logger) {
assert.NotNil(t, logger)
logger.Info("End-to-end test log message")
}),
SyncOnShutdown(),
}

if test.err {
app := fx.New(opts...)
require.NotNil(t, app)
assert.Error(t, app.Err())
} else {
app := fxtest.New(t, opts...)
require.NotNil(t, app)
assert.NoError(t, app.Err())
app.RequireStart().RequireStop()
}

// Close the writer and restore stderr
w.Close()
os.Stderr = oldStderr
<-done
})
}
}
37 changes: 0 additions & 37 deletions logger.go

This file was deleted.

Loading
Loading