Skip to content

Commit ee4a8a9

Browse files
committed
Initial commit
1 parent 5681aad commit ee4a8a9

23 files changed

+883
-1
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
11
# aws-embedded-metrics-golang
2-
Go implementation of AWS CloudWatch Embedded Metric Format
2+
3+
Golang implementation of AWS CloudWatch [Embedded Metric Format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html)
4+
5+
It's aim is to simplify reporting metrics to CloudWatch:
6+
- using EMF avoids additional HTTP calls as it just logs JSON to stdout
7+
- built in support for Lambda functions default dimensions and properties (happy to accept PR for EC2 support)
8+
- no need for defining additional dependencies (or mocks in tests) to report metrics from inside your code
9+
10+
Examples:
11+
```
12+
emf.New().Namespace("mtg").Metric("totalWins", 1500).Log()
13+
14+
emf.New().Dimension("colour", "red").
15+
MetricAs("gameLength", 2, emf.Seconds).Log()
16+
17+
emf.New().DimensionSet(emf.NewDimension("format", "edh"), emf.NewDimension("commander", "Muldrotha")).
18+
MetricAs("wins", 1499, emf.Count).Log()
19+
```
20+

emf/emf.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Spec available here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
2+
package emf
3+
4+
// Metadata struct as defined in AWS Embedded Metrics Format spec.
5+
type Metadata struct {
6+
Timestamp int64 `json:"Timestamp"`
7+
Metrics []MetricDirective `json:"CloudWatchMetrics"`
8+
}
9+
10+
// MetricDirective struct as defined in AWS Embedded Metrics Format spec.
11+
type MetricDirective struct {
12+
Namespace string `json:"Namespace"`
13+
Dimensions []DimensionSet `json:"Dimensions"`
14+
Metrics []MetricDefinition `json:"Metrics"`
15+
}
16+
17+
// DimensionSet as defined in AWS Embedded Metrics Format spec.
18+
type DimensionSet []string
19+
20+
// MetricDefinition struct as defined in AWS Embedded Metrics Format spec.
21+
type MetricDefinition struct {
22+
Name string `json:"Name"`
23+
Unit MetricUnit `json:"Unit,omitempty"`
24+
}

emf/logger.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package emf
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strings"
9+
"time"
10+
)
11+
12+
// Logger for metrics with default Context.
13+
type Logger struct {
14+
out io.Writer
15+
timestamp int64
16+
defaultContext Context
17+
contexts []*Context
18+
values map[string]interface{}
19+
}
20+
21+
// Context gives ability to add another MetricDirective section for Logger.
22+
type Context struct {
23+
metricDirective MetricDirective
24+
values map[string]interface{}
25+
}
26+
27+
// New creates logger printing to os.Stdout, perfect for Lambda functions.
28+
func New() *Logger {
29+
return NewFor(os.Stdout)
30+
}
31+
32+
// NewFor creates logger printing to any suitable writer.
33+
func NewFor(out io.Writer) *Logger {
34+
values := make(map[string]interface{})
35+
36+
// set default properties for lambda function
37+
fnName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME")
38+
if fnName != "" {
39+
values["executionEnvironment"] = os.Getenv("AWS_EXECUTION_ENV")
40+
values["memorySize"] = os.Getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")
41+
values["functionVersion"] = os.Getenv("AWS_LAMBDA_FUNCTION_VERSION")
42+
values["logStreamId"] = os.Getenv("AWS_LAMBDA_LOG_STREAM_NAME")
43+
}
44+
45+
// only collect traces which have been sampled
46+
amznTraceID := os.Getenv("_X_AMZN_TRACE_ID")
47+
if strings.Contains(amznTraceID, "Sampled=1") {
48+
values["traceId"] = amznTraceID
49+
}
50+
51+
return &Logger{
52+
out: out,
53+
defaultContext: newContext(values),
54+
values: values,
55+
timestamp: time.Now().UnixNano() / int64(time.Millisecond),
56+
}
57+
}
58+
59+
// Dimension helps builds DimensionSet.
60+
type Dimension struct {
61+
Key, Value string
62+
}
63+
64+
// NewDimension creates Dimension from key/value pair.
65+
func NewDimension(key, value string) Dimension {
66+
return Dimension{
67+
Key: key,
68+
Value: value,
69+
}
70+
}
71+
72+
// Namespace sets namespace on default context.
73+
func (l *Logger) Namespace(namespace string) *Logger {
74+
l.defaultContext.Namespace(namespace)
75+
return l
76+
}
77+
78+
// Property sets property.
79+
func (l *Logger) Property(key, value string) *Logger {
80+
l.values[key] = value
81+
return l
82+
}
83+
84+
// Dimension adds single dimension on default context.
85+
func (l *Logger) Dimension(key, value string) *Logger {
86+
l.defaultContext.metricDirective.Dimensions = append(
87+
l.defaultContext.metricDirective.Dimensions, DimensionSet{key})
88+
l.values[key] = value
89+
return l
90+
}
91+
92+
// DimensionSet adds multiple dimensions on default context.
93+
func (l *Logger) DimensionSet(dimensions ...Dimension) *Logger {
94+
var set DimensionSet
95+
for _, d := range dimensions {
96+
set = append(set, d.Key)
97+
l.values[d.Key] = d.Value
98+
}
99+
l.defaultContext.metricDirective.Dimensions = append(
100+
l.defaultContext.metricDirective.Dimensions, set)
101+
return l
102+
}
103+
104+
// Metric puts int metric on default context.
105+
func (l *Logger) Metric(name string, value int) *Logger {
106+
l.defaultContext.put(name, value, None)
107+
return l
108+
}
109+
110+
// MetricFloat puts float metric on default context.
111+
func (l *Logger) MetricFloat(name string, value float64) *Logger {
112+
l.defaultContext.put(name, value, None)
113+
return l
114+
}
115+
116+
// MetricAs puts int metric with MetricUnit on default context.
117+
func (l *Logger) MetricAs(name string, value int, unit MetricUnit) *Logger {
118+
l.defaultContext.put(name, value, unit)
119+
return l
120+
}
121+
122+
// MetricFloatAs puts float metric with MetricUnit on default context.
123+
func (l *Logger) MetricFloatAs(name string, value float64, unit MetricUnit) *Logger {
124+
l.defaultContext.put(name, value, unit)
125+
return l
126+
}
127+
128+
// Log prints all Contexts and metric values to chosen output in Embedded Metric Format.
129+
func (l *Logger) Log() {
130+
var metrics []MetricDirective
131+
if len(l.defaultContext.metricDirective.Metrics) > 0 {
132+
metrics = append(metrics, l.defaultContext.metricDirective)
133+
}
134+
for _, v := range l.contexts {
135+
// TODO check if not empty as above?
136+
metrics = append(metrics, v.metricDirective)
137+
}
138+
l.values["_aws"] = Metadata{
139+
Timestamp: l.timestamp,
140+
Metrics: metrics,
141+
}
142+
buf, _ := json.Marshal(l.values)
143+
_, _ = fmt.Fprintln(l.out, string(buf))
144+
}
145+
146+
// NewContext creates new context for given logger.
147+
func (l *Logger) NewContext() *Context {
148+
c := newContext(l.values)
149+
l.contexts = append(l.contexts, &c)
150+
return &c
151+
}
152+
153+
// Namespace sets namespace on given context.
154+
func (c *Context) Namespace(namespace string) *Context {
155+
c.metricDirective.Namespace = namespace
156+
return c
157+
}
158+
159+
// Dimension adds single dimension on given context.
160+
func (c *Context) Dimension(key, value string) *Context {
161+
c.metricDirective.Dimensions = append(c.metricDirective.Dimensions, DimensionSet{key})
162+
c.values[key] = value
163+
return c
164+
}
165+
166+
// DimensionSet adds multiple dimensions on given context.
167+
func (c *Context) DimensionSet(dimensions ...Dimension) *Context {
168+
var set DimensionSet
169+
for _, d := range dimensions {
170+
set = append(set, d.Key)
171+
c.values[d.Key] = d.Value
172+
}
173+
c.metricDirective.Dimensions = append(c.metricDirective.Dimensions, set)
174+
return c
175+
}
176+
177+
// Metric puts int metric on given context.
178+
func (c *Context) Metric(name string, value int) *Context {
179+
return c.put(name, value, None)
180+
}
181+
182+
// MetricFloat puts float metric on given context.
183+
func (c *Context) MetricFloat(name string, value float64) *Context {
184+
return c.put(name, value, None)
185+
}
186+
187+
// MetricAs puts int metric with MetricUnit on given context.
188+
func (c *Context) MetricAs(name string, value int, unit MetricUnit) *Context {
189+
return c.put(name, value, unit)
190+
}
191+
192+
// MetricFloatAs puts float metric with MetricUnit on given context.
193+
func (c *Context) MetricFloatAs(name string, value float64, unit MetricUnit) *Context {
194+
return c.put(name, value, unit)
195+
}
196+
197+
func newContext(values map[string]interface{}) Context {
198+
var defaultDimensions []DimensionSet
199+
200+
// set default dimensions for lambda function
201+
fnName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME")
202+
if fnName != "" {
203+
defaultDimensions = []DimensionSet{{"ServiceName", "ServiceType"}}
204+
values["ServiceType"] = "AWS::Lambda::Function"
205+
values["ServiceName"] = fnName
206+
}
207+
208+
return Context{
209+
metricDirective: MetricDirective{
210+
Namespace: "aws-embedded-metrics",
211+
Dimensions: defaultDimensions,
212+
},
213+
values: values,
214+
}
215+
}
216+
217+
func (c *Context) put(name string, value interface{}, unit MetricUnit) *Context {
218+
c.metricDirective.Metrics = append(c.metricDirective.Metrics, MetricDefinition{
219+
Name: name,
220+
Unit: unit,
221+
})
222+
c.values[name] = value
223+
return c
224+
}

0 commit comments

Comments
 (0)