Skip to content

Commit ab46f9c

Browse files
Allow customization of the logger with NewWith
1 parent 96448d4 commit ab46f9c

File tree

5 files changed

+131
-10
lines changed

5 files changed

+131
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
# Dependency directories (remove the comment below to include it)
1515
# vendor/
1616

17+
# IDE artifacts
1718
.idea/

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
Go implementation of AWS CloudWatch [Embedded Metric Format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html)
77

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

1617
Usage:
18+
1719
```
1820
emf.New().Namespace("mtg").Metric("totalWins", 1500).Log()
1921
2022
emf.New().Dimension("colour", "red").
2123
MetricAs("gameLength", 2, emf.Seconds).Log()
2224
2325
emf.New().DimensionSet(
24-
emf.NewDimension("format", "edh"),
26+
emf.NewDimension("format", "edh"),
2527
emf.NewDimension("commander", "Muldrotha")).
2628
MetricAs("wins", 1499, emf.Count).Log()
2729
```
2830

2931
You may also use the lib together with `defer`.
32+
3033
```
3134
m := emf.New() // sets up whatever you fancy here
3235
defer m.Log()
3336
3437
// any reporting metrics calls
3538
```
3639

40+
Customizing the logger:
41+
```
42+
emf.NewWith(
43+
emf.WithWriter(os.Stderr), // Log to stderr.
44+
emf.WithTimestamp(time.Now().Add(-time.Hour)), // Record past metrics.
45+
)
46+
```
47+
3748
Functions for reporting metrics:
49+
3850
```
3951
func Metric(name string, value int)
4052
func Metrics(m map[string]int)
@@ -48,6 +60,7 @@ func MetricsFloatAs(m map[string]float64, unit MetricUnit)
4860
```
4961

5062
Functions for setting up dimensions:
63+
5164
```
5265
func Dimension(key, value string)
5366
func DimensionSet(dimensions ...Dimension) // use `func NewDimension` for creating one

emf/emf.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Spec available here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
1+
// Package emf implements the spec available here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
22
package emf
33

44
// Metadata struct as defined in AWS Embedded Metrics Format spec.

emf/logger.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,29 @@ type Context struct {
2424
values map[string]interface{}
2525
}
2626

27-
// New creates logger printing to os.Stdout, perfect for Lambda functions.
28-
func New() *Logger {
29-
return NewFor(os.Stdout)
27+
// NewOption defines a function that can be used to customize a logger.
28+
type NewOption func(l *Logger)
29+
30+
// WithWriter customizes the writer used by a logger.
31+
func WithWriter(w io.Writer) NewOption {
32+
return func(l *Logger) {
33+
l.out = w
34+
}
3035
}
3136

32-
// NewFor creates logger printing to any suitable writer.
33-
func NewFor(out io.Writer) *Logger {
37+
// WithTimestamp customizes the timestamp used by a logger.
38+
func WithTimestamp(t time.Time) NewOption {
39+
return func(l *Logger) {
40+
l.timestamp = t.UnixNano() / int64(time.Millisecond)
41+
}
42+
}
43+
44+
// NewWith creates logger with reasonable defaults for Lambda functions:
45+
// - Prints to os.Stdout.
46+
// - Context based on Lambda environment variables.
47+
// - Timestamp set to the time when NewWith was called.
48+
// Specify NewOptions to customize the logger.
49+
func NewWith(opts ...NewOption) *Logger {
3450
values := make(map[string]interface{})
3551

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

51-
return &Logger{
52-
out: out,
67+
// create a default logger
68+
l := &Logger{
69+
out: os.Stdout,
5370
defaultContext: newContext(values),
5471
values: values,
5572
timestamp: time.Now().UnixNano() / int64(time.Millisecond),
5673
}
74+
75+
// apply any options
76+
for _, opt := range opts {
77+
opt(l)
78+
}
79+
80+
return l
81+
}
82+
83+
// New creates logger printing to os.Stdout, perfect for Lambda functions.
84+
// Deprecated: use NewWith instead.
85+
func New() *Logger {
86+
return NewWith(WithWriter(os.Stdout))
87+
}
88+
89+
// NewFor creates logger printing to any suitable writer.
90+
// Deprecated: use NewWith and WithWriter instead.
91+
func NewFor(out io.Writer) *Logger {
92+
return NewWith(WithWriter(out))
5793
}
5894

5995
// Dimension helps builds DimensionSet.
@@ -129,7 +165,7 @@ func (l *Logger) MetricAs(name string, value int, unit MetricUnit) *Logger {
129165
return l
130166
}
131167

132-
// Metrics puts all of the int metrics with MetricUnit on default context.
168+
// MetricsAs puts all of the int metrics with MetricUnit on default context.
133169
func (l *Logger) MetricsAs(m map[string]int, unit MetricUnit) *Logger {
134170
for name, value := range m {
135171
l.defaultContext.put(name, value, unit)

emf/logger_internal_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package emf
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestNewWith(t *testing.T) {
11+
tcs := []struct {
12+
name string
13+
opts []NewOption
14+
expected *Logger
15+
}{
16+
{
17+
name: "default",
18+
expected: &Logger{
19+
out: os.Stdout,
20+
timestamp: time.Now().UnixNano() / int64(time.Millisecond),
21+
},
22+
},
23+
{
24+
name: "with options",
25+
opts: []NewOption{
26+
WithWriter(os.Stderr),
27+
WithTimestamp(time.Now().Add(time.Hour)),
28+
},
29+
expected: &Logger{
30+
out: os.Stderr,
31+
timestamp: time.Now().Add(time.Hour).UnixNano() / int64(time.Millisecond),
32+
},
33+
},
34+
}
35+
36+
for _, tc := range tcs {
37+
t.Run(tc.name, func(t *testing.T) {
38+
actual := NewWith(tc.opts...)
39+
if err := loggersEqual(actual, tc.expected); err != nil {
40+
t.Errorf("logger does not match: %v", err)
41+
}
42+
})
43+
}
44+
45+
}
46+
47+
// loggersEqual returns a non-nil error if the loggers do not match.
48+
// Currently it only checks that the loggers' output writer and timestamp match.
49+
// TODO: expand the checks here as more NewOptions are added.
50+
func loggersEqual(actual, expected *Logger) error {
51+
if actual.out != expected.out {
52+
return fmt.Errorf("output does not match")
53+
}
54+
55+
if err := approxInt64(actual.timestamp, expected.timestamp, 100 /* ms */); err != nil {
56+
return fmt.Errorf("timestamp %v", err)
57+
}
58+
59+
return nil
60+
}
61+
62+
func approxInt64(actual, expected, tolerance int64) error {
63+
diff := expected - actual
64+
if diff < 0 {
65+
diff = -diff
66+
}
67+
if diff > tolerance {
68+
return fmt.Errorf("value %v is out of tolerance %v±%v", actual, expected, tolerance)
69+
}
70+
return nil
71+
}

0 commit comments

Comments
 (0)