├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── emf ├── emf.go ├── logger.go ├── logger_internal_test.go ├── logger_test.go ├── testdata │ ├── 1.json │ ├── 10.json │ ├── 11.json │ ├── 12.json │ ├── 13.json │ ├── 14.json │ ├── 15.json │ ├── 16.json │ ├── 17.json │ ├── 18.json │ ├── 2.json │ ├── 3.json │ ├── 4.json │ ├── 5.json │ ├── 6.json │ ├── 7.json │ ├── 8.json │ └── 9.json └── units.go ├── go.mod └── go.sum /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v2 15 | - uses: actions/checkout@v2 16 | - name: golangci-lint 17 | uses: golangci/golangci-lint-action@v2 18 | with: 19 | version: latest 20 | working-directory: emf 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: set up Go 1.x 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ^1.14 18 | id: go 19 | - name: checkout 20 | uses: actions/checkout@v2 21 | - name: modules 22 | run: go mod download 23 | - name: test 24 | run: go test -v ./emf 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDE artifacts 18 | .idea/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 prozz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-embedded-metrics-golang 2 | 3 | ![test](https://github.com/prozz/aws-embedded-metrics-golang/workflows/test/badge.svg?branch=master) 4 | ![golangci-lint](https://github.com/prozz/aws-embedded-metrics-golang/workflows/lint/badge.svg?branch=master) 5 | 6 | Go implementation of AWS CloudWatch [Embedded Metric Format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html) 7 | 8 | It's aim is to simplify reporting metrics to CloudWatch: 9 | 10 | - using EMF avoids additional HTTP API calls to CloudWatch as metrics are logged in JSON format to stdout 11 | - no need for additional dependencies in your services (or mocks in tests) to report metrics from inside your code 12 | - built in support for default dimensions and properties for Lambda functions 13 | 14 | Supports namespaces, setting dimensions and properties as well as different contexts (at least partially). 15 | 16 | ## Installation 17 | 18 | ```shell 19 | go get github.com/prozz/aws-embedded-metrics-golang 20 | ``` 21 | 22 | ## Usage 23 | 24 | ``` 25 | emf.New().Namespace("mtg").Metric("totalWins", 1500).Log() 26 | 27 | emf.New().Dimension("colour", "red"). 28 | MetricAs("gameLength", 2, emf.Seconds).Log() 29 | 30 | emf.New().DimensionSet( 31 | emf.NewDimension("format", "edh"), 32 | emf.NewDimension("commander", "Muldrotha")). 33 | MetricAs("wins", 1499, emf.Count).Log() 34 | ``` 35 | 36 | You may also use the lib together with `defer`. 37 | 38 | ``` 39 | m := emf.New() // sets up whatever you fancy here 40 | defer m.Log() 41 | 42 | // any reporting metrics calls 43 | ``` 44 | 45 | Customizing the logger: 46 | ``` 47 | emf.New( 48 | emf.WithWriter(os.Stderr), // Log to stderr. 49 | emf.WithTimestamp(time.Now().Add(-time.Hour)), // Record past metrics. 50 | emf.WithoutDimensions(), // Do not include useful Lambda related dimensions. 51 | emf.WithLogGroup("my-logs") // Add specific log group. 52 | ) 53 | ``` 54 | 55 | Functions for reporting metrics: 56 | 57 | ``` 58 | func Metric(name string, value int) 59 | func Metrics(m map[string]int) 60 | func MetricAs(name string, value int, unit MetricUnit) 61 | func MetricsAs(m map[string]int, unit MetricUnit) 62 | 63 | func MetricFloat(name string, value float64) 64 | func MetricsFloat(m map[string]float64) 65 | func MetricFloatAs(name string, value float64, unit MetricUnit) 66 | func MetricsFloatAs(m map[string]float64, unit MetricUnit) 67 | ``` 68 | 69 | Functions for setting up dimensions: 70 | 71 | ``` 72 | func Dimension(key, value string) 73 | func DimensionSet(dimensions ...Dimension) // use `func NewDimension` for creating one 74 | ``` 75 | 76 | ## Contributing 77 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 78 | Please make sure to update tests. 79 | 80 | ## License 81 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /emf/emf.go: -------------------------------------------------------------------------------- 1 | // Package emf implements the 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 | LogGroupName string `json:"LogGroupName,omitempty"` 9 | } 10 | 11 | // MetricDirective struct as defined in AWS Embedded Metrics Format spec. 12 | type MetricDirective struct { 13 | Namespace string `json:"Namespace"` 14 | Dimensions []DimensionSet `json:"Dimensions"` 15 | Metrics []MetricDefinition `json:"Metrics"` 16 | } 17 | 18 | // DimensionSet as defined in AWS Embedded Metrics Format spec. 19 | type DimensionSet []string 20 | 21 | // MetricDefinition struct as defined in AWS Embedded Metrics Format spec. 22 | type MetricDefinition struct { 23 | Name string `json:"Name"` 24 | Unit MetricUnit `json:"Unit,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /emf/logger.go: -------------------------------------------------------------------------------- 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 | withoutDimensions bool 20 | logGroupName string 21 | } 22 | 23 | // Context gives ability to add another MetricDirective section for Logger. 24 | type Context struct { 25 | metricDirective MetricDirective 26 | values map[string]interface{} 27 | } 28 | 29 | // LoggerOption defines a function that can be used to customize a logger. 30 | type LoggerOption func(l *Logger) 31 | 32 | // WithWriter customizes the writer used by a logger. 33 | func WithWriter(w io.Writer) LoggerOption { 34 | return func(l *Logger) { 35 | l.out = w 36 | } 37 | } 38 | 39 | // WithTimestamp customizes the timestamp used by a logger. 40 | func WithTimestamp(t time.Time) LoggerOption { 41 | return func(l *Logger) { 42 | l.timestamp = t.UnixNano() / int64(time.Millisecond) 43 | } 44 | } 45 | 46 | // WithoutDimensions ignores default AWS Lambda related properties and dimensions. 47 | func WithoutDimensions() LoggerOption { 48 | return func(l *Logger) { 49 | l.withoutDimensions = true 50 | } 51 | } 52 | 53 | // WithLogGroup sets the log group when ingesting metrics into Cloudwatch Logging Agent. 54 | func WithLogGroup(logGroup string) LoggerOption { 55 | return func(l *Logger) { 56 | l.logGroupName = logGroup 57 | } 58 | } 59 | 60 | // New creates logger with reasonable defaults for Lambda functions: 61 | // - Prints to os.Stdout. 62 | // - Context based on Lambda environment variables. 63 | // - Timestamp set to the time when New was called. 64 | // Specify LoggerOptions to customize the logger. 65 | func New(opts ...LoggerOption) *Logger { 66 | l := Logger{ 67 | out: os.Stdout, 68 | timestamp: time.Now().UnixNano() / int64(time.Millisecond), 69 | } 70 | 71 | // apply any options 72 | for _, opt := range opts { 73 | opt(&l) 74 | } 75 | 76 | values := make(map[string]interface{}) 77 | 78 | if !l.withoutDimensions { 79 | // set default properties for lambda function 80 | fnName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME") 81 | if fnName != "" { 82 | values["executionEnvironment"] = os.Getenv("AWS_EXECUTION_ENV") 83 | values["memorySize"] = os.Getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") 84 | values["functionVersion"] = os.Getenv("AWS_LAMBDA_FUNCTION_VERSION") 85 | values["logStreamId"] = os.Getenv("AWS_LAMBDA_LOG_STREAM_NAME") 86 | } 87 | } 88 | 89 | // only collect traces which have been sampled 90 | amznTraceID := os.Getenv("_X_AMZN_TRACE_ID") 91 | if strings.Contains(amznTraceID, "Sampled=1") { 92 | values["traceId"] = amznTraceID 93 | } 94 | 95 | l.values = values 96 | l.defaultContext = newContext(values, l.withoutDimensions) 97 | 98 | return &l 99 | } 100 | 101 | // Dimension helps builds DimensionSet. 102 | type Dimension struct { 103 | Key, Value string 104 | } 105 | 106 | // NewDimension creates Dimension from key/value pair. 107 | func NewDimension(key, value string) Dimension { 108 | return Dimension{ 109 | Key: key, 110 | Value: value, 111 | } 112 | } 113 | 114 | // Namespace sets namespace on default context. 115 | func (l *Logger) Namespace(namespace string) *Logger { 116 | l.defaultContext.Namespace(namespace) 117 | return l 118 | } 119 | 120 | // Property sets property. 121 | func (l *Logger) Property(key, value string) *Logger { 122 | l.values[key] = value 123 | return l 124 | } 125 | 126 | // Dimension adds single dimension on default context. 127 | func (l *Logger) Dimension(key, value string) *Logger { 128 | l.defaultContext.metricDirective.Dimensions = append( 129 | l.defaultContext.metricDirective.Dimensions, DimensionSet{key}) 130 | l.values[key] = value 131 | return l 132 | } 133 | 134 | // DimensionSet adds multiple dimensions on default context. 135 | func (l *Logger) DimensionSet(dimensions ...Dimension) *Logger { 136 | var set DimensionSet 137 | for _, d := range dimensions { 138 | set = append(set, d.Key) 139 | l.values[d.Key] = d.Value 140 | } 141 | l.defaultContext.metricDirective.Dimensions = append( 142 | l.defaultContext.metricDirective.Dimensions, set) 143 | return l 144 | } 145 | 146 | // Metric puts int metric on default context. 147 | func (l *Logger) Metric(name string, value int) *Logger { 148 | l.defaultContext.put(name, value, None) 149 | return l 150 | } 151 | 152 | // Metrics puts all of the int metrics on default context. 153 | func (l *Logger) Metrics(m map[string]int) *Logger { 154 | return l.MetricsAs(m, None) 155 | } 156 | 157 | // MetricFloat puts float metric on default context. 158 | func (l *Logger) MetricFloat(name string, value float64) *Logger { 159 | l.defaultContext.put(name, value, None) 160 | return l 161 | } 162 | 163 | // MetricsFloat puts all of the float metrics on default context. 164 | func (l *Logger) MetricsFloat(m map[string]float64) *Logger { 165 | return l.MetricsFloatAs(m, None) 166 | } 167 | 168 | // MetricAs puts int metric with MetricUnit on default context. 169 | func (l *Logger) MetricAs(name string, value int, unit MetricUnit) *Logger { 170 | l.defaultContext.put(name, value, unit) 171 | return l 172 | } 173 | 174 | // MetricsAs puts all of the int metrics with MetricUnit on default context. 175 | func (l *Logger) MetricsAs(m map[string]int, unit MetricUnit) *Logger { 176 | for name, value := range m { 177 | l.defaultContext.put(name, value, unit) 178 | } 179 | return l 180 | } 181 | 182 | // MetricFloatAs puts float metric with MetricUnit on default context. 183 | func (l *Logger) MetricFloatAs(name string, value float64, unit MetricUnit) *Logger { 184 | l.defaultContext.put(name, value, unit) 185 | return l 186 | } 187 | 188 | // MetricsFloatAs puts all of the float metrics with MetricUnit on default context. 189 | func (l *Logger) MetricsFloatAs(m map[string]float64, unit MetricUnit) *Logger { 190 | for name, value := range m { 191 | l.defaultContext.put(name, value, unit) 192 | } 193 | return l 194 | } 195 | 196 | // Log prints all Contexts and metric values to chosen output in Embedded Metric Format. 197 | func (l *Logger) Log() { 198 | var metrics []MetricDirective 199 | if len(l.defaultContext.metricDirective.Metrics) > 0 { 200 | metrics = append(metrics, l.defaultContext.metricDirective) 201 | } 202 | for _, v := range l.contexts { 203 | if len(v.metricDirective.Metrics) > 0 { 204 | metrics = append(metrics, v.metricDirective) 205 | } 206 | } 207 | 208 | if len(metrics) == 0 { 209 | return 210 | } 211 | 212 | l.values["_aws"] = Metadata{ 213 | Timestamp: l.timestamp, 214 | Metrics: metrics, 215 | LogGroupName: l.logGroupName, 216 | } 217 | buf, _ := json.Marshal(l.values) 218 | _, _ = fmt.Fprintln(l.out, string(buf)) 219 | } 220 | 221 | // NewContext creates new context for given logger. 222 | func (l *Logger) NewContext() *Context { 223 | c := newContext(l.values, l.withoutDimensions) 224 | l.contexts = append(l.contexts, &c) 225 | return &c 226 | } 227 | 228 | // Namespace sets namespace on given context. 229 | func (c *Context) Namespace(namespace string) *Context { 230 | c.metricDirective.Namespace = namespace 231 | return c 232 | } 233 | 234 | // Dimension adds single dimension on given context. 235 | func (c *Context) Dimension(key, value string) *Context { 236 | c.metricDirective.Dimensions = append(c.metricDirective.Dimensions, DimensionSet{key}) 237 | c.values[key] = value 238 | return c 239 | } 240 | 241 | // DimensionSet adds multiple dimensions on given context. 242 | func (c *Context) DimensionSet(dimensions ...Dimension) *Context { 243 | var set DimensionSet 244 | for _, d := range dimensions { 245 | set = append(set, d.Key) 246 | c.values[d.Key] = d.Value 247 | } 248 | c.metricDirective.Dimensions = append(c.metricDirective.Dimensions, set) 249 | return c 250 | } 251 | 252 | // Metric puts int metric on given context. 253 | func (c *Context) Metric(name string, value int) *Context { 254 | return c.put(name, value, None) 255 | } 256 | 257 | // MetricFloat puts float metric on given context. 258 | func (c *Context) MetricFloat(name string, value float64) *Context { 259 | return c.put(name, value, None) 260 | } 261 | 262 | // MetricAs puts int metric with MetricUnit on given context. 263 | func (c *Context) MetricAs(name string, value int, unit MetricUnit) *Context { 264 | return c.put(name, value, unit) 265 | } 266 | 267 | // MetricFloatAs puts float metric with MetricUnit on given context. 268 | func (c *Context) MetricFloatAs(name string, value float64, unit MetricUnit) *Context { 269 | return c.put(name, value, unit) 270 | } 271 | 272 | func newContext(values map[string]interface{}, withoutDimensions bool) Context { 273 | var defaultDimensions []DimensionSet 274 | if !withoutDimensions { 275 | // set default dimensions for lambda function 276 | fnName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME") 277 | if fnName != "" { 278 | defaultDimensions = []DimensionSet{{"ServiceName", "ServiceType"}} 279 | values["ServiceType"] = "AWS::Lambda::Function" 280 | values["ServiceName"] = fnName 281 | } 282 | } 283 | 284 | return Context{ 285 | metricDirective: MetricDirective{ 286 | Namespace: "aws-embedded-metrics", 287 | Dimensions: defaultDimensions, 288 | }, 289 | values: values, 290 | } 291 | } 292 | 293 | func (c *Context) put(name string, value interface{}, unit MetricUnit) *Context { 294 | c.metricDirective.Metrics = append(c.metricDirective.Metrics, MetricDefinition{ 295 | Name: name, 296 | Unit: unit, 297 | }) 298 | c.values[name] = value 299 | return c 300 | } 301 | -------------------------------------------------------------------------------- /emf/logger_internal_test.go: -------------------------------------------------------------------------------- 1 | package emf 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | tcs := []struct { 12 | name string 13 | opts []LoggerOption 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: []LoggerOption{ 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 | name: "without dimensions", 36 | opts: []LoggerOption{ 37 | WithoutDimensions(), 38 | }, 39 | expected: &Logger{ 40 | out: os.Stdout, 41 | timestamp: time.Now().UnixNano() / int64(time.Millisecond), 42 | withoutDimensions: false, 43 | }, 44 | }, 45 | } 46 | 47 | for _, tc := range tcs { 48 | t.Run(tc.name, func(t *testing.T) { 49 | actual := New(tc.opts...) 50 | if err := loggersEqual(actual, tc.expected); err != nil { 51 | t.Errorf("logger does not match: %v", err) 52 | } 53 | }) 54 | } 55 | 56 | } 57 | 58 | // loggersEqual returns a non-nil error if the loggers do not match. 59 | // Currently it only checks that the loggers' output writer and timestamp match. 60 | func loggersEqual(actual, expected *Logger) error { 61 | if actual.out != expected.out { 62 | return fmt.Errorf("output does not match") 63 | } 64 | 65 | if err := approxInt64(actual.timestamp, expected.timestamp, 100 /* ms */); err != nil { 66 | return fmt.Errorf("timestamp %v", err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func approxInt64(actual, expected, tolerance int64) error { 73 | diff := expected - actual 74 | if diff < 0 { 75 | diff = -diff 76 | } 77 | if diff > tolerance { 78 | return fmt.Errorf("value %v is out of tolerance %v±%v", actual, expected, tolerance) 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /emf/logger_test.go: -------------------------------------------------------------------------------- 1 | package emf_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/kinbiko/jsonassert" 10 | "github.com/prozz/aws-embedded-metrics-golang/emf" 11 | ) 12 | 13 | func TestEmf(t *testing.T) { 14 | tcs := []struct { 15 | name string 16 | new func(*bytes.Buffer) *emf.Logger 17 | env map[string]string 18 | given func(*emf.Logger) 19 | expected string 20 | }{ 21 | { 22 | name: "default namespace, int metric", 23 | given: func(logger *emf.Logger) { 24 | logger.Metric("foo", 33) 25 | }, 26 | expected: "testdata/1.json", 27 | }, 28 | { 29 | name: "default namespace, float metric", 30 | given: func(logger *emf.Logger) { 31 | logger.MetricFloat("foo", 33.66) 32 | }, 33 | expected: "testdata/2.json", 34 | }, 35 | { 36 | name: "custom namespace, int and float metrics", 37 | given: func(logger *emf.Logger) { 38 | logger.Namespace("galaxy").MetricFloat("foo", 33.66).Metric("bar", 666) 39 | }, 40 | expected: "testdata/3.json", 41 | }, 42 | { 43 | name: "custom namespace, int and float metrics, custom units", 44 | given: func(logger *emf.Logger) { 45 | logger.Namespace("galaxy"). 46 | MetricFloatAs("foo", 33.66, emf.Milliseconds). 47 | MetricAs("bar", 666, emf.Count) 48 | }, 49 | expected: "testdata/4.json", 50 | }, 51 | { 52 | name: "new context, default namespace, int and float metrics, custom units", 53 | given: func(logger *emf.Logger) { 54 | logger.NewContext(). 55 | MetricFloatAs("foo", 33.66, emf.Milliseconds). 56 | MetricAs("bar", 666, emf.Count) 57 | }, 58 | expected: "testdata/5.json", 59 | }, 60 | { 61 | name: "new context, custom namespace, int and float metrics, custom units", 62 | given: func(logger *emf.Logger) { 63 | logger.NewContext().Namespace("galaxy"). 64 | MetricFloatAs("foo", 33.66, emf.Bits). 65 | MetricAs("bar", 666, emf.BytesSecond) 66 | }, 67 | expected: "testdata/6.json", 68 | }, 69 | { 70 | name: "default and custom contexts, different metrics names", 71 | given: func(logger *emf.Logger) { 72 | logger.NewContext().MetricFloatAs("foo", 33.66, emf.Bits) 73 | logger.MetricAs("bar", 666, emf.BytesSecond) 74 | }, 75 | expected: "testdata/7.json", 76 | }, 77 | { 78 | name: "set property", 79 | given: func(logger *emf.Logger) { 80 | logger.Property("aaa", "666").Metric("foo", 33) 81 | }, 82 | expected: "testdata/8.json", 83 | }, 84 | { 85 | name: "set default properties for lambda", 86 | env: map[string]string{ 87 | "AWS_LAMBDA_FUNCTION_NAME": "some-func-name", 88 | "AWS_EXECUTION_ENV": "golang", 89 | "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "128", 90 | "AWS_LAMBDA_FUNCTION_VERSION": "1", 91 | "AWS_LAMBDA_LOG_STREAM_NAME": "log/stream", 92 | }, 93 | given: func(logger *emf.Logger) { 94 | logger.Metric("foo", 33) 95 | }, 96 | expected: "testdata/9.json", 97 | }, 98 | { 99 | name: "not sampled trace", 100 | env: map[string]string{ 101 | "_X_AMZN_TRACE_ID": "foo", 102 | }, 103 | given: func(logger *emf.Logger) { 104 | logger.Metric("foo", 33) 105 | }, 106 | expected: "testdata/10.json", 107 | }, 108 | { 109 | name: "sampled trace", 110 | env: map[string]string{ 111 | "_X_AMZN_TRACE_ID": "foo,Sampled=1,bar", 112 | }, 113 | given: func(logger *emf.Logger) { 114 | logger.Metric("foo", 33) 115 | }, 116 | expected: "testdata/11.json", 117 | }, 118 | { 119 | name: "one dimension", 120 | given: func(logger *emf.Logger) { 121 | logger.Dimension("a", "b").Metric("c", 11) 122 | }, 123 | expected: "testdata/12.json", 124 | }, 125 | { 126 | name: "two dimensions", 127 | given: func(logger *emf.Logger) { 128 | logger.Dimension("a", "b").Dimension("o", "p").Metric("c", 11) 129 | }, 130 | expected: "testdata/13.json", 131 | }, 132 | { 133 | name: "one dimension set", 134 | given: func(logger *emf.Logger) { 135 | logger. 136 | DimensionSet( 137 | emf.NewDimension("a", "b"), 138 | emf.NewDimension("o", "p")). 139 | Metric("c", 11) 140 | }, 141 | expected: "testdata/14.json", 142 | }, 143 | { 144 | name: "two dimension sets", 145 | given: func(logger *emf.Logger) { 146 | logger. 147 | DimensionSet( 148 | emf.NewDimension("a", "b"), 149 | emf.NewDimension("c", "d")). 150 | DimensionSet( 151 | emf.NewDimension("1", "2"), 152 | emf.NewDimension("3", "4")). 153 | Metric("foo", 22) 154 | }, 155 | expected: "testdata/15.json", 156 | }, 157 | { 158 | name: "default and custom contexts, multiple dimensions/dimension sets", 159 | given: func(logger *emf.Logger) { 160 | logger.NewContext(). 161 | Dimension("CC", "DD"). 162 | DimensionSet( 163 | emf.NewDimension("gg", "hh"), 164 | emf.NewDimension("kk", "jj")). 165 | MetricFloatAs("foo", 33.66, emf.Bits) 166 | logger. 167 | Dimension("AA", "BB"). 168 | DimensionSet( 169 | emf.NewDimension("ww", "ee"), 170 | emf.NewDimension("rr", "tt")). 171 | MetricAs("bar", 666, emf.BytesSecond) 172 | }, 173 | expected: "testdata/16.json", 174 | }, 175 | { 176 | name: "default properties and dimensions for lambda are ignored", 177 | new: func(buf *bytes.Buffer) *emf.Logger { 178 | return emf.New(emf.WithoutDimensions(), emf.WithWriter(buf)) 179 | }, 180 | env: map[string]string{ 181 | "AWS_LAMBDA_FUNCTION_NAME": "some-func-name", 182 | "AWS_EXECUTION_ENV": "golang", 183 | "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "128", 184 | "AWS_LAMBDA_FUNCTION_VERSION": "1", 185 | "AWS_LAMBDA_LOG_STREAM_NAME": "log/stream", 186 | }, 187 | given: func(logger *emf.Logger) { 188 | logger.Metric("foo", 33) 189 | }, 190 | expected: "testdata/17.json", 191 | }, 192 | { 193 | name: "with log group", 194 | new: func(buf *bytes.Buffer) *emf.Logger { 195 | return emf.New(emf.WithLogGroup("test_log_group"), emf.WithWriter(buf)) 196 | }, 197 | given: func(logger *emf.Logger) { 198 | logger.Metric("foo", 33) 199 | }, 200 | expected: "testdata/18.json", 201 | }, 202 | } 203 | 204 | for _, tc := range tcs { 205 | t.Run(tc.name, func(t *testing.T) { 206 | if len(tc.env) > 0 { 207 | defer unsetenv(t, tc.env) 208 | setenv(t, tc.env) 209 | } 210 | 211 | var buf bytes.Buffer 212 | var logger *emf.Logger 213 | if tc.new != nil { 214 | logger = tc.new(&buf) 215 | } else { 216 | logger = emf.New(emf.WithWriter(&buf)) 217 | } 218 | tc.given(logger) 219 | logger.Log() 220 | 221 | println(buf.String()) 222 | f, err := ioutil.ReadFile(tc.expected) 223 | if err != nil { 224 | t.Fatal("unable to read file with expected json") 225 | } 226 | 227 | jsonassert.New(t).Assertf(buf.String(), string(f)) 228 | }) 229 | } 230 | 231 | t.Run("no metrics set", func(t *testing.T) { 232 | var buf bytes.Buffer 233 | logger := emf.New(emf.WithWriter(&buf)) 234 | logger.Log() 235 | 236 | if buf.String() != "" { 237 | t.Error("Buffer not empty") 238 | } 239 | }) 240 | 241 | t.Run("new context, no metrics set", func(t *testing.T) { 242 | var buf bytes.Buffer 243 | logger := emf.New(emf.WithWriter(&buf)) 244 | logger.NewContext().Namespace("galaxy") 245 | logger.Log() 246 | 247 | if buf.String() != "" { 248 | t.Error("Buffer not empty") 249 | } 250 | }) 251 | } 252 | 253 | func setenv(t *testing.T, env map[string]string) { 254 | for k, v := range env { 255 | err := os.Setenv(k, v) 256 | if err != nil { 257 | t.Fatalf("unable to set env variable: %s", k) 258 | } 259 | } 260 | } 261 | 262 | func unsetenv(t *testing.T, env map[string]string) { 263 | for k := range env { 264 | err := os.Unsetenv(k) 265 | if err != nil { 266 | t.Fatalf("unable to unset env variable: %s", k) 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /emf/testdata/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | } 15 | ] 16 | }, 17 | "foo": 33 18 | } -------------------------------------------------------------------------------- /emf/testdata/10.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | } 15 | ] 16 | }, 17 | "foo": 33 18 | } -------------------------------------------------------------------------------- /emf/testdata/11.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | } 15 | ] 16 | }, 17 | "foo": 33, 18 | "traceId": "foo,Sampled=1,bar" 19 | } -------------------------------------------------------------------------------- /emf/testdata/12.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "c", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": [["a"]] 14 | } 15 | ] 16 | }, 17 | "a": "b", 18 | "c": 11 19 | } -------------------------------------------------------------------------------- /emf/testdata/13.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "c", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": [["a"], ["o"]] 14 | } 15 | ] 16 | }, 17 | "a": "b", 18 | "o": "p", 19 | "c": 11 20 | } -------------------------------------------------------------------------------- /emf/testdata/14.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "c", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": [["a", "o"]] 14 | } 15 | ] 16 | }, 17 | "a": "b", 18 | "o": "p", 19 | "c": 11 20 | } -------------------------------------------------------------------------------- /emf/testdata/15.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": [["a", "c"], ["1", "3"]] 14 | } 15 | ] 16 | }, 17 | "a": "b", 18 | "c": "d", 19 | "1": "2", 20 | "3": "4", 21 | "foo": 22 22 | } -------------------------------------------------------------------------------- /emf/testdata/16.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "bar", 9 | "Unit": "Bytes/Second" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": [["AA"], ["ww", "rr"]] 14 | }, 15 | { 16 | "Metrics": [ 17 | { 18 | "Name": "foo", 19 | "Unit": "Bits" 20 | } 21 | ], 22 | "Namespace": "aws-embedded-metrics", 23 | "Dimensions": [["CC"], ["gg", "kk"]] 24 | } 25 | ] 26 | }, 27 | "foo": 33.66, 28 | "bar": 666, 29 | "CC": "DD", 30 | "gg": "hh", 31 | "kk": "jj", 32 | "AA": "BB", 33 | "ww": "ee", 34 | "rr": "tt" 35 | } -------------------------------------------------------------------------------- /emf/testdata/17.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | } 15 | ] 16 | }, 17 | "foo": 33 18 | } -------------------------------------------------------------------------------- /emf/testdata/18.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "LogGroupName": "test_log_group", 5 | "CloudWatchMetrics": [ 6 | { 7 | "Metrics": [ 8 | { 9 | "Name": "foo", 10 | "Unit": "None" 11 | } 12 | ], 13 | "Namespace": "aws-embedded-metrics", 14 | "Dimensions": null 15 | } 16 | ] 17 | }, 18 | "foo": 33 19 | } -------------------------------------------------------------------------------- /emf/testdata/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | } 15 | ] 16 | }, 17 | "foo": 33.66 18 | } -------------------------------------------------------------------------------- /emf/testdata/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | }, 11 | { 12 | "Name": "bar", 13 | "Unit": "None" 14 | } 15 | ], 16 | "Namespace": "galaxy", 17 | "Dimensions": null 18 | } 19 | ] 20 | }, 21 | "foo": 33.66, 22 | "bar": 666 23 | } -------------------------------------------------------------------------------- /emf/testdata/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "Milliseconds" 10 | }, 11 | { 12 | "Name": "bar", 13 | "Unit": "Count" 14 | } 15 | ], 16 | "Namespace": "galaxy", 17 | "Dimensions": null 18 | } 19 | ] 20 | }, 21 | "foo": 33.66, 22 | "bar": 666 23 | } -------------------------------------------------------------------------------- /emf/testdata/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "Milliseconds" 10 | }, 11 | { 12 | "Name": "bar", 13 | "Unit": "Count" 14 | } 15 | ], 16 | "Namespace": "aws-embedded-metrics", 17 | "Dimensions": null 18 | } 19 | ] 20 | }, 21 | "foo": 33.66, 22 | "bar": 666 23 | } -------------------------------------------------------------------------------- /emf/testdata/6.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "Bits" 10 | }, 11 | { 12 | "Name": "bar", 13 | "Unit": "Bytes/Second" 14 | } 15 | ], 16 | "Namespace": "galaxy", 17 | "Dimensions": null 18 | } 19 | ] 20 | }, 21 | "foo": 33.66, 22 | "bar": 666 23 | } -------------------------------------------------------------------------------- /emf/testdata/7.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "bar", 9 | "Unit": "Bytes/Second" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | }, 15 | { 16 | "Metrics": [ 17 | { 18 | "Name": "foo", 19 | "Unit": "Bits" 20 | } 21 | ], 22 | "Namespace": "aws-embedded-metrics", 23 | "Dimensions": null 24 | } 25 | ] 26 | }, 27 | "foo": 33.66, 28 | "bar": 666 29 | } -------------------------------------------------------------------------------- /emf/testdata/8.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": null 14 | } 15 | ] 16 | }, 17 | "foo": 33, 18 | "aaa": "666" 19 | } -------------------------------------------------------------------------------- /emf/testdata/9.json: -------------------------------------------------------------------------------- 1 | { 2 | "_aws": { 3 | "Timestamp": "<>", 4 | "CloudWatchMetrics": [ 5 | { 6 | "Metrics": [ 7 | { 8 | "Name": "foo", 9 | "Unit": "None" 10 | } 11 | ], 12 | "Namespace": "aws-embedded-metrics", 13 | "Dimensions": [ 14 | [ 15 | "ServiceName", 16 | "ServiceType" 17 | ] 18 | ] 19 | } 20 | ] 21 | }, 22 | "ServiceName": "some-func-name", 23 | "ServiceType": "AWS::Lambda::Function", 24 | "executionEnvironment": "golang", 25 | "memorySize": "128", 26 | "functionVersion": "1", 27 | "logStreamId": "log/stream", 28 | "foo": 33 29 | } -------------------------------------------------------------------------------- /emf/units.go: -------------------------------------------------------------------------------- 1 | package emf 2 | 3 | type MetricUnit string 4 | 5 | const ( 6 | None MetricUnit = "None" 7 | Seconds MetricUnit = "Seconds" 8 | Microseconds MetricUnit = "Microseconds" 9 | Milliseconds MetricUnit = "Milliseconds" 10 | Bytes MetricUnit = "Bytes" 11 | Kilobytes MetricUnit = "Kilobytes" 12 | Megabytes MetricUnit = "Megabytes" 13 | Gigabytes MetricUnit = "Gigabytes" 14 | Terabytes MetricUnit = "Terabytes" 15 | Bits MetricUnit = "Bits" 16 | Kilobits MetricUnit = "Kilobits" 17 | Megabits MetricUnit = "Megabits" 18 | Gigabits MetricUnit = "Gigabits" 19 | Terabits MetricUnit = "Terabits" 20 | Percent MetricUnit = "Percent" 21 | Count MetricUnit = "Count" 22 | BytesSecond MetricUnit = "Bytes/Second" 23 | KilobytesSecond MetricUnit = "Kilobytes/Second" 24 | MegabytesSecond MetricUnit = "Megabytes/Second" 25 | GigabytesSecond MetricUnit = "Gigabytes/Second" 26 | TerabytesSecond MetricUnit = "Terabytes/Second" 27 | BitsSecond MetricUnit = "Bits/Second" 28 | KilobitsSecond MetricUnit = "Kilobits/Second" 29 | MegabitsSecond MetricUnit = "Megabits/Second" 30 | GigabitsSecond MetricUnit = "Gigabits/Second" 31 | TerabitsSecond MetricUnit = "Terabits/Second" 32 | CountSecond MetricUnit = "Count/Second" 33 | ) 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prozz/aws-embedded-metrics-golang 2 | 3 | go 1.14 4 | 5 | require github.com/kinbiko/jsonassert v1.1.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kinbiko/jsonassert v1.1.1 h1:DB12divY+YB+cVpHULLuKePSi6+ui4M/shHSzJISkSE= 2 | github.com/kinbiko/jsonassert v1.1.1/go.mod h1:NO4lzrogohtIdNUNzx8sdzB55M4R4Q1bsrWVdqQ7C+A= 3 | --------------------------------------------------------------------------------