Skip to content

Commit

Permalink
Merge pull request #4 from harrisonhjones/feature-allow-customization…
Browse files Browse the repository at this point in the history
…-of-logger

Allow customization of the logger with NewWith
  • Loading branch information
prozz authored Feb 15, 2021
2 parents 96448d4 + 63a3716 commit e4d4fd5
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 15 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v1
uses: golangci/golangci-lint-action@v2
with:
version: v1.26
version: v1.29
working-directory: emf
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/

# IDE artifacts
.idea/
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Go implementation of AWS CloudWatch [Embedded Metric Format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html)

It's aim is to simplify reporting metrics to CloudWatch:

- using EMF avoids additional HTTP API calls to CloudWatch as metrics are logged in JSON format to stdout
- no need for additional dependencies in your services (or mocks in tests) to report metrics from inside your code
- built in support for default dimensions and properties for Lambda functions
Expand All @@ -14,27 +15,38 @@ It's aim is to simplify reporting metrics to CloudWatch:
Supports namespaces, setting dimensions and properties as well as different contexts (at least partially).

Usage:

```
emf.New().Namespace("mtg").Metric("totalWins", 1500).Log()
emf.New().Dimension("colour", "red").
MetricAs("gameLength", 2, emf.Seconds).Log()
emf.New().DimensionSet(
emf.NewDimension("format", "edh"),
emf.NewDimension("format", "edh"),
emf.NewDimension("commander", "Muldrotha")).
MetricAs("wins", 1499, emf.Count).Log()
```

You may also use the lib together with `defer`.

```
m := emf.New() // sets up whatever you fancy here
defer m.Log()
// any reporting metrics calls
```

Customizing the logger:
```
emf.New(
emf.WithWriter(os.Stderr), // Log to stderr.
emf.WithTimestamp(time.Now().Add(-time.Hour)), // Record past metrics.
)
```

Functions for reporting metrics:

```
func Metric(name string, value int)
func Metrics(m map[string]int)
Expand All @@ -48,6 +60,7 @@ func MetricsFloatAs(m map[string]float64, unit MetricUnit)
```

Functions for setting up dimensions:

```
func Dimension(key, value string)
func DimensionSet(dimensions ...Dimension) // use `func NewDimension` for creating one
Expand Down
2 changes: 1 addition & 1 deletion emf/emf.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Spec available here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
// Package emf implements the spec available here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
package emf

// Metadata struct as defined in AWS Embedded Metrics Format spec.
Expand Down
40 changes: 32 additions & 8 deletions emf/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,29 @@ type Context struct {
values map[string]interface{}
}

// New creates logger printing to os.Stdout, perfect for Lambda functions.
func New() *Logger {
return NewFor(os.Stdout)
// LoggerOption defines a function that can be used to customize a logger.
type LoggerOption func(l *Logger)

// WithWriter customizes the writer used by a logger.
func WithWriter(w io.Writer) LoggerOption {
return func(l *Logger) {
l.out = w
}
}

// WithTimestamp customizes the timestamp used by a logger.
func WithTimestamp(t time.Time) LoggerOption {
return func(l *Logger) {
l.timestamp = t.UnixNano() / int64(time.Millisecond)
}
}

// NewFor creates logger printing to any suitable writer.
func NewFor(out io.Writer) *Logger {
// New creates logger with reasonable defaults for Lambda functions:
// - Prints to os.Stdout.
// - Context based on Lambda environment variables.
// - Timestamp set to the time when New was called.
// Specify LoggerOptions to customize the logger.
func New(opts ...LoggerOption) *Logger {
values := make(map[string]interface{})

// set default properties for lambda function
Expand All @@ -48,12 +64,20 @@ func NewFor(out io.Writer) *Logger {
values["traceId"] = amznTraceID
}

return &Logger{
out: out,
// create a default logger
l := &Logger{
out: os.Stdout,
defaultContext: newContext(values),
values: values,
timestamp: time.Now().UnixNano() / int64(time.Millisecond),
}

// apply any options
for _, opt := range opts {
opt(l)
}

return l
}

// Dimension helps builds DimensionSet.
Expand Down Expand Up @@ -129,7 +153,7 @@ func (l *Logger) MetricAs(name string, value int, unit MetricUnit) *Logger {
return l
}

// Metrics puts all of the int metrics with MetricUnit on default context.
// MetricsAs puts all of the int metrics with MetricUnit on default context.
func (l *Logger) MetricsAs(m map[string]int, unit MetricUnit) *Logger {
for name, value := range m {
l.defaultContext.put(name, value, unit)
Expand Down
70 changes: 70 additions & 0 deletions emf/logger_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package emf

import (
"fmt"
"os"
"testing"
"time"
)

func TestNew(t *testing.T) {
tcs := []struct {
name string
opts []LoggerOption
expected *Logger
}{
{
name: "default",
expected: &Logger{
out: os.Stdout,
timestamp: time.Now().UnixNano() / int64(time.Millisecond),
},
},
{
name: "with options",
opts: []LoggerOption{
WithWriter(os.Stderr),
WithTimestamp(time.Now().Add(time.Hour)),
},
expected: &Logger{
out: os.Stderr,
timestamp: time.Now().Add(time.Hour).UnixNano() / int64(time.Millisecond),
},
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
actual := New(tc.opts...)
if err := loggersEqual(actual, tc.expected); err != nil {
t.Errorf("logger does not match: %v", err)
}
})
}

}

// loggersEqual returns a non-nil error if the loggers do not match.
// Currently it only checks that the loggers' output writer and timestamp match.
func loggersEqual(actual, expected *Logger) error {
if actual.out != expected.out {
return fmt.Errorf("output does not match")
}

if err := approxInt64(actual.timestamp, expected.timestamp, 100 /* ms */); err != nil {
return fmt.Errorf("timestamp %v", err)
}

return nil
}

func approxInt64(actual, expected, tolerance int64) error {
diff := expected - actual
if diff < 0 {
diff = -diff
}
if diff > tolerance {
return fmt.Errorf("value %v is out of tolerance %v±%v", actual, expected, tolerance)
}
return nil
}
6 changes: 3 additions & 3 deletions emf/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func TestEmf(t *testing.T) {
}

var buf bytes.Buffer
logger := emf.NewFor(&buf)
logger := emf.New(emf.WithWriter(&buf))
tc.given(logger)
logger.Log()

Expand All @@ -196,7 +196,7 @@ func TestEmf(t *testing.T) {

t.Run("no metrics set", func(t *testing.T) {
var buf bytes.Buffer
logger := emf.NewFor(&buf)
logger := emf.New(emf.WithWriter(&buf))
logger.Log()

if buf.String() != "" {
Expand All @@ -206,7 +206,7 @@ func TestEmf(t *testing.T) {

t.Run("new context, no metrics set", func(t *testing.T) {
var buf bytes.Buffer
logger := emf.NewFor(&buf)
logger := emf.New(emf.WithWriter(&buf))
logger.NewContext().Namespace("galaxy")
logger.Log()

Expand Down

0 comments on commit e4d4fd5

Please sign in to comment.