├── .github ├── CODEOWNERS ├── codecov.yml └── workflows │ └── ci.yaml ├── go.mod ├── .licenserignore ├── .gitignore ├── group ├── go.mod ├── go.sum ├── service_test.go └── service.go ├── level_test.go ├── logger_test.go ├── noop.go ├── level.go ├── README.md ├── noop_test.go ├── global_metricsink.go ├── metric_test.go ├── Makefile ├── .golangci.yml ├── doc.go ├── logger.go ├── function ├── logger_test.go └── logger.go ├── scope ├── scope_test.go └── scope.go ├── metric.go └── LICENSE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tetratelabs/oss-utils-admin 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/telemetry 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /.licenserignore: -------------------------------------------------------------------------------- 1 | /LICENSE 2 | .idea 3 | go.mod 4 | go.sum 5 | build/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .project 3 | .vscode 4 | .DS_Store 5 | .vimrc 6 | build/ 7 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | # we build and upload only a single coverage file, so we don't need to wait for other CI 3 | # jobs to complete for us to see the coverage results 4 | require_ci_to_pass: false 5 | 6 | # not interested in inline PR comments via github checks 7 | github_checks: false 8 | 9 | comment: 10 | layout: "diff,files" 11 | 12 | coverage: 13 | status: 14 | # require coverage to not be worse than previously 15 | project: 16 | default: 17 | target: auto 18 | # allow a potential drop of up to 5% 19 | threshold: 5% 20 | -------------------------------------------------------------------------------- /group/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/telemetry/group 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/tetratelabs/log v0.2.1 7 | github.com/tetratelabs/multierror v1.1.0 8 | github.com/tetratelabs/run v0.1.2 9 | github.com/tetratelabs/telemetry v0.7.1 10 | ) 11 | 12 | require ( 13 | github.com/hashicorp/errwrap v1.1.0 // indirect 14 | github.com/logrusorgru/aurora v2.0.3+incompatible // indirect 15 | github.com/oklog/run v1.1.0 // indirect 16 | github.com/spf13/pflag v1.0.5 // indirect 17 | ) 18 | 19 | // Work around for maintaining multiple go modules in the same repository 20 | // until go has better support for this. https://github.com/golang/go/issues/45713 21 | replace github.com/tetratelabs/telemetry => ../ 22 | -------------------------------------------------------------------------------- /group/go.sum: -------------------------------------------------------------------------------- 1 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 2 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 3 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 4 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 5 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 6 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 7 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 8 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 9 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 10 | github.com/tetratelabs/log v0.2.1 h1:/nfvV1GT+ixUGIO9Ad1ZjHkkcs6fJquFUiQHiHHYkxU= 11 | github.com/tetratelabs/log v0.2.1/go.mod h1:GV7LrNKonPeIj1g9nPNhZOL23w94MyXiQth9YJOi5cE= 12 | github.com/tetratelabs/multierror v1.1.0 h1:cKmV/Pbf42K5wp8glxa2YIausbxIraPN8fzru9Pn1Cg= 13 | github.com/tetratelabs/multierror v1.1.0/go.mod h1:kH3SzI/z+FwEbV9bxQDx4GiIgE2djuyb8wiB2DaUBnY= 14 | github.com/tetratelabs/run v0.1.2 h1:rV4fz7XgKnVqzYppy7SCeKGQjXdUQPq5/jHrZRQj88c= 15 | github.com/tetratelabs/run v0.1.2/go.mod h1:0M/NMEnauGFXm6/zVP4RMpJBNzGW7sLi+d4/pgc2CNw= 16 | -------------------------------------------------------------------------------- /level_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) Tetrate, Inc 2022 All Rights Reserved. 16 | 17 | package telemetry 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestFromLevel(t *testing.T) { 24 | tests := []struct { 25 | level string 26 | want Level 27 | ok bool 28 | }{ 29 | {"none", LevelNone, true}, 30 | {"error", LevelError, true}, 31 | {"info", LevelInfo, true}, 32 | {"debug", LevelDebug, true}, 33 | {"invalid", LevelNone, false}, 34 | } 35 | 36 | for _, tt := range tests { 37 | t.Run(tt.level, func(t *testing.T) { 38 | level, ok := FromLevel(tt.level) 39 | 40 | if level != tt.want { 41 | t.Fatalf("AsLevel(%s)=%s, want: %s", tt.level, level, tt.want) 42 | } 43 | if ok != tt.ok { 44 | t.Fatalf("AsLevel(%s)=%t, want: %t", tt.level, ok, tt.ok) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import ( 18 | "context" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestContext(t *testing.T) { 24 | want := []interface{}{"key1", "val1"} 25 | ctx := context.Background() 26 | 27 | ctx = KeyValuesToContext(ctx, want...) 28 | have := KeyValuesFromContext(ctx) 29 | 30 | if !reflect.DeepEqual(want, have) { 31 | t.Errorf("want: %+v\nhave: %+v\n", want, have) 32 | } 33 | } 34 | 35 | func TestRemoveFromContext(t *testing.T) { 36 | want := []interface{}{"key1", "val2"} 37 | ctx := context.Background() 38 | 39 | ctx = KeyValuesToContext(ctx, "key1", "val1") 40 | ctx = RemoveKeyValuesFromContext(ctx) 41 | ctx = KeyValuesToContext(ctx, want...) 42 | have := KeyValuesFromContext(ctx) 43 | 44 | if !reflect.DeepEqual(want, have) { 45 | t.Errorf("want: %+v\nhave: %+v\n", want, have) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /noop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import "context" 18 | 19 | // NoopLogger returns a no-op logger. 20 | func NoopLogger() Logger { 21 | return &noopLogger{level: LevelNone} 22 | } 23 | 24 | type noopLogger struct { 25 | level Level 26 | } 27 | 28 | func (*noopLogger) Debug(string, ...interface{}) {} 29 | func (*noopLogger) Info(string, ...interface{}) {} 30 | func (n noopLogger) Error(string, error, ...interface{}) {} 31 | func (n *noopLogger) SetLevel(l Level) { n.level = l } 32 | func (n *noopLogger) Level() Level { return n.level } 33 | func (n *noopLogger) With(...interface{}) Logger { return n } 34 | func (n *noopLogger) Context(context.Context) Logger { return n } 35 | func (n *noopLogger) Metric(Metric) Logger { return n } 36 | func (n *noopLogger) Clone() Logger { return NoopLogger() } 37 | -------------------------------------------------------------------------------- /level.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | // Level is an enumeration of the available log levels. 18 | type Level int32 19 | 20 | // Available log levels. 21 | const ( 22 | LevelNone Level = 0 23 | LevelError Level = 1 24 | LevelInfo Level = 5 25 | LevelDebug Level = 10 26 | ) 27 | 28 | // levelToString maps each logging level to its string representation. 29 | var levelToString = map[Level]string{ 30 | LevelNone: "none", 31 | LevelError: "error", 32 | LevelInfo: "info", 33 | LevelDebug: "debug", 34 | } 35 | 36 | // levelToString maps string representations to the corresponding level 37 | var stringToLevel = map[string]Level{ 38 | "none": LevelNone, 39 | "error": LevelError, 40 | "info": LevelInfo, 41 | "debug": LevelDebug, 42 | } 43 | 44 | // String returns the string representation of the logging level. 45 | func (v Level) String() string { return levelToString[v] } 46 | 47 | // FromLevel returns the logging level corresponding to the given string representation. 48 | func FromLevel(level string) (Level, bool) { 49 | l, ok := stringToLevel[level] 50 | return l, ok 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telemetry 2 | 3 | [![CI](https://github.com/tetratelabs/telemetry/actions/workflows/ci.yaml/badge.svg)](https://github.com/tetratelabs/telemetry/actions/workflows/ci.yaml) 4 | [![codecov](https://codecov.io/gh/tetratelabs/telemetry/branch/master/graph/badge.svg?token=CLZMDX2TUN)](https://codecov.io/gh/tetratelabs/telemetry) 5 | 6 | This package provides a set of Telemetry interfaces allowing you to completely 7 | decouple your libraries and packages from Logging and Metrics instrumentation 8 | implementations. 9 | 10 | For more information on the interfaces, see: 11 | https://pkg.go.dev/github.com/tetratelabs/telemetry 12 | 13 | ## Implementations 14 | 15 | Below you can find a list of known interface implementations. As a consumer of 16 | this package, you might want to select existing implementations to use in your 17 | application binaries. If looking to write your own, take a look at existing ones 18 | for inspiration. 19 | 20 | If you have an OSS implementation you want to share, feel free to submit a PR 21 | to this file so it may be included in this list. 22 | 23 | | repository | supported interfaces | notes | 24 | | --- | --- | --- | 25 | | tetratelabs/[telemetry-gokit-log](https://github.com/tetratelabs/telemetry-gokit-log) | Logger | [Go kit log](https://github.com/go-kit/log) bridge | 26 | | tetratelabs/[log](https://github.com/tetratelabs/log/tree/v2) | Logger | Scoped structured/unstructured logger bridge | 27 | | tetratelabs/[telemetry-opencensus](https://github.com/tetratelabs/telemetry-opencensus) | Metrics | [OpenCensus metrics](https://github.com/census-instrumentation/opencensus-go) bridge | 28 | | tetratelabs/[telemetry-opentelemetry](https://github.com/tetratelabs/telemetry-opentelemetry) | Metrics | [OpenTelemetry metrics](https://opentelemetry.io/) bridge | 29 | -------------------------------------------------------------------------------- /noop_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "testing" 21 | ) 22 | 23 | func TestNoopLogger(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | logfunc func(Logger) 27 | metricCount float64 28 | }{ 29 | {"info-", func(l Logger) { l.Info("text", "where", "there") }, 1}, 30 | {"error", func(l Logger) { l.Error("text", errors.New("error"), "where", "there") }, 1}, 31 | {"debug", func(l Logger) { l.Debug("text", "where", "there") }, 0}, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | 37 | metric := mockMetric{} 38 | ctx := KeyValuesToContext(context.Background(), "ctx", "value") 39 | l := NoopLogger().Context(ctx).Metric(&metric).With().With("lvl", LevelInfo).With("missing") 40 | 41 | tt.logfunc(l) 42 | if metric.count != 0 { 43 | t.Fatalf("metric.count=%v, want 0", metric.count) 44 | } 45 | 46 | l.SetLevel(LevelDebug) 47 | if l.Level() != LevelDebug { 48 | t.Fatalf("l.Level()=%v, want LevelDebug", l.Level()) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | type mockMetric struct { 55 | Metric 56 | count float64 57 | } 58 | 59 | func (m *mockMetric) RecordContext(_ context.Context, value float64) { m.count += value } 60 | -------------------------------------------------------------------------------- /global_metricsink.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | // OnGlobalMetricSinkFn holds a function signature which can be used to register 22 | // Metric bootstrapping that needs to be called after the GlobalMetricSink has 23 | // been registered. 24 | type OnGlobalMetricSinkFn func(m MetricSink) 25 | 26 | var ( 27 | mtx sync.Mutex 28 | metricSink MetricSink 29 | callbacks []func(MetricSink) 30 | ) 31 | 32 | // SetGlobalMetricSink allows one to set a global MetricSink, after which all 33 | // registered OnGlobalMetricSinkFn callback functions are executed. 34 | func SetGlobalMetricSink(ms MetricSink) { 35 | mtx.Lock() 36 | defer mtx.Unlock() 37 | 38 | metricSink = ms 39 | for _, callback := range callbacks { 40 | callback(ms) 41 | } 42 | callbacks = nil 43 | } 44 | 45 | // ToGlobalMetricSink allows one to set callback functions to bootstrap Metrics 46 | // as soon as the Global MetricSink has been registered. If the MetricSink has 47 | // already been registered, this callback will happen immediately. 48 | func ToGlobalMetricSink(callback OnGlobalMetricSinkFn) { 49 | mtx.Lock() 50 | defer mtx.Unlock() 51 | 52 | if metricSink != nil { 53 | callback(metricSink) 54 | return 55 | } 56 | 57 | callbacks = append(callbacks, callback) 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Tetrate 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # yamllint --format github .github/workflows/ci.yaml 16 | --- 17 | name: CI 18 | 19 | on: [push, pull_request] 20 | 21 | env: 22 | GOPROXY: https://proxy.golang.org 23 | GOVERSION: 1.17.2 # must match go.mod 24 | 25 | jobs: 26 | sanity: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/setup-go@v2 30 | with: 31 | go-version: ${{ env.GOVERSION }} 32 | - uses: actions/checkout@v2 33 | - run: make check 34 | 35 | build: 36 | needs: sanity 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/setup-go@v2 40 | with: 41 | go-version: ${{ env.GOVERSION }} 42 | - uses: actions/checkout@v2 43 | - run: make build 44 | 45 | test: 46 | needs: sanity 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/setup-go@v2 50 | with: 51 | go-version: ${{ env.GOVERSION }} 52 | - uses: actions/checkout@v2 53 | - run: make test 54 | 55 | coverage: 56 | needs: build 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/setup-go@v2 60 | with: 61 | go-version: ${{ env.GOVERSION }} 62 | - uses: actions/checkout@v2 63 | - run: make coverage 64 | - uses: codecov/codecov-action@v2 65 | with: 66 | token: ${{ secrets.CODECOV_TOKEN }} 67 | files: ./build/coverage.out 68 | 69 | lint: 70 | needs: sanity 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/setup-go@v2 74 | with: 75 | go-version: ${{ env.GOVERSION }} 76 | - uses: actions/checkout@v2 77 | - run: make lint 78 | -------------------------------------------------------------------------------- /metric_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry_test 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/tetratelabs/telemetry" 21 | ) 22 | 23 | var _ telemetry.Label = (*label)(nil) 24 | 25 | type metricSink struct { 26 | options telemetry.MetricOptions 27 | } 28 | 29 | type label string 30 | 31 | func (l label) Insert(string) telemetry.LabelValue { return nil } 32 | func (l label) Update(string) telemetry.LabelValue { return nil } 33 | func (l label) Upsert(string) telemetry.LabelValue { return nil } 34 | func (l label) Delete() telemetry.LabelValue { return nil } 35 | 36 | func newMetricSink(options ...telemetry.MetricOption) *metricSink { 37 | var ms metricSink 38 | for _, opt := range options { 39 | opt(&ms.options) 40 | } 41 | return &ms 42 | } 43 | 44 | func TestMetricOptions(t *testing.T) { 45 | label1 := label("label1") 46 | label2 := label("label2") 47 | ms := newMetricSink( 48 | telemetry.WithUnit(telemetry.Milliseconds), 49 | telemetry.WithEnabled(func() bool { return true }), 50 | telemetry.WithLabels(label1, label2), 51 | ) 52 | 53 | if ms.options.EnabledCondition == nil { 54 | t.Fatal("expected EnabledCondition to hold a function") 55 | } 56 | if !ms.options.EnabledCondition() { 57 | t.Errorf("expected EnabledCondition function to return true") 58 | } 59 | if ms.options.Unit != telemetry.Milliseconds { 60 | t.Errorf("expected Unit to be ms (milliseconds)") 61 | } 62 | if len(ms.options.Labels) != 2 { 63 | t.Fatalf("unexpected label count: want: 2, have: %d", len(ms.options.Labels)) 64 | } 65 | if ms.options.Labels[0] != label1 { 66 | t.Errorf("[0] unexpected label value: want: %s, have: %s", label1, ms.options.Labels[0].(label)) 67 | } 68 | if ms.options.Labels[1] != label2 { 69 | t.Errorf("[1] unexpected label value: want: %s, have: %s", label2, ms.options.Labels[1].(label)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) Tetrate, Inc 2023. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http:#www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | MODULE_PATH ?= $(shell sed -ne 's/^module //gp' go.mod) 16 | 17 | # Tools 18 | LINTER := github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43.0 19 | LICENSER := github.com/liamawhite/licenser@v0.6.1-0.20210729145742-be6c77bf6a1f 20 | GOIMPORTS := golang.org/x/tools/cmd/goimports@v0.1.5 21 | 22 | # List of available module subdirs. 23 | SUBDIRS := . group 24 | 25 | .PHONY: build 26 | build: 27 | $(call run,go build ./...) 28 | 29 | TEST_OPTS ?= -race 30 | .PHONY: test 31 | test: 32 | $(call run,go test $(TEST_OPTS) ./...) 33 | 34 | BENCH_OPTS ?= 35 | .PHONY: bench 36 | bench: 37 | $(call run,go test -bench=. $(BENCH_OPTS) ./...) 38 | 39 | .PHONY: coverage 40 | coverage: 41 | mkdir -p build 42 | go test -coverprofile build/coverage.out -covermode atomic -coverpkg '$(MODULE_PATH)/...' ./... 43 | go tool cover -o build/coverage.html -html build/coverage.out 44 | # TODO(dio): Need to provide coverage for group. 45 | 46 | LINT_CONFIG := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))).golangci.yml 47 | LINT_OPTS ?= --timeout 5m 48 | .PHONY: lint 49 | lint: 50 | $(call run,go run $(LINTER) run $(LINT_OPTS) --config $(LINT_CONFIG)) 51 | 52 | GO_SOURCES = $(shell git ls-files | grep '.go$$') 53 | .PHONY: format 54 | format: 55 | @for f in $(GO_SOURCES); do \ 56 | awk '/^import \($$/,/^\)$$/{if($$0=="")next}{print}' "$$f" > /tmp/fmt; \ 57 | mv /tmp/fmt "$$f"; \ 58 | done 59 | go run $(GOIMPORTS) -w -local $(MODULE_PATH) $(GO_SOURCES) 60 | go run $(LICENSER) apply -r "Tetrate" 61 | 62 | .PHONY: check 63 | check: 64 | @$(MAKE) format 65 | $(call run,go mod tidy) 66 | @if [ ! -z "`git status -s`" ]; then \ 67 | echo "The following differences will fail CI until committed:"; \ 68 | git diff; \ 69 | exit 1; \ 70 | fi 71 | 72 | # Run command defined in the first arg of this function in each defined subdir. 73 | define run 74 | for DIR in $(SUBDIRS); do \ 75 | cd $$DIR && $1; \ 76 | done 77 | endef 78 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Tetrate, Inc 2023. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | linters: 15 | enable: 16 | - contextcheck 17 | - deadcode 18 | - errcheck 19 | - errname 20 | - goconst 21 | - gosimple 22 | - govet 23 | - ineffassign 24 | - lll 25 | - megacheck 26 | - misspell 27 | - prealloc 28 | - revive 29 | - staticcheck 30 | - structcheck 31 | - tenv 32 | - unconvert 33 | - unused 34 | - varcheck 35 | disable: 36 | - interfacer # deprecated since it "is prone to bad suggestions" 37 | - goimports # already run in `make format` 38 | linters-settings: 39 | lll: 40 | line-length: 160 41 | goconst: 42 | min-occurrences: 4 43 | govet: 44 | check-shadowing: true 45 | revive: 46 | ignore-generated-header: false 47 | severity: error 48 | confidence: 0.8 49 | rules: 50 | - name: atomic 51 | - name: blank-imports 52 | - name: confusing-naming 53 | - name: get-return 54 | - name: context-as-argument 55 | - name: context-keys-type 56 | - name: dot-imports 57 | # A true empty block is hard to recognize, see https://github.com/mgechev/revive/issues/386 58 | #- name: empty-block 59 | - name: error-naming 60 | - name: error-return 61 | - name: error-strings 62 | - name: errorf 63 | - name: exported 64 | - name: if-return 65 | - name: increment-decrement 66 | - name: indent-error-flow 67 | - name: package-comments 68 | - name: range 69 | - name: receiver-naming 70 | - name: redefines-builtin-id 71 | - name: superfluous-else 72 | - name: struct-tag 73 | - name: time-naming 74 | - name: unexported-naming 75 | - name: unexported-return 76 | - name: unhandled-error 77 | arguments: [ 78 | "fmt.Printf", 79 | "fmt.Println", 80 | "fmt.Fprintf", 81 | ] 82 | - name: unreachable-code 83 | - name: unused-parameter 84 | - name: var-declaration 85 | - name: var-naming 86 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package telemetry holds observability facades for our services and libraries. 17 | 18 | The provided interface here allows for instrumenting libraries and packages 19 | without any dependencies on Logging and Metric instrumentation implementations. 20 | This allows a consistent way of authoring Log lines and Metrics for the 21 | producers of these libraries and packages while providing consumers the ability 22 | to plug in the implementations of their choice. 23 | 24 | The following requirements helped shape the form of the interfaces. 25 | 26 | - Simple to use! 27 | 28 | Or developers will resort to using `fmt.Printf()` 29 | 30 | - No elaborate amount of logging levels. 31 | 32 | Error: something happened that we can't gracefully recover from. 33 | This is a log line that should be actionable by an operator and be 34 | alerted on. 35 | 36 | Info: something happened that might be of interest but does not impact 37 | the application stability. E.g. someone gave the wrong credentials and 38 | was therefore denied access, parsing error on external input, etc. 39 | 40 | Debug: anything that can help to understand application state during 41 | development. 42 | 43 | More levels get tricky to reason about when writing log lines or establishing 44 | the right level of verbosity at runtime. By the above explanations fatal folds 45 | into error, warning folds into info, and trace folds into debug. 46 | 47 | We trust more in partitioning loggers per domain, component, etc. and allow them 48 | to be individually addressed to required log levels than controlling a single 49 | logger with more levels. 50 | 51 | We also believe that most logs should be metrics. Anything above Debug level 52 | should be able to emit a metric which can be use for dashboards, alerting, etc. 53 | 54 | - Structured logging from the interface side. 55 | 56 | We want the ability to rollup / aggregate over the same message while allowing 57 | for contextual data to be added. A logging implementation can make the choice 58 | how to present to provided log data. This can be 100% structured, a single log 59 | line, or a combination. 60 | 61 | - Allow pass through of contextual values. 62 | 63 | Allow the Go Context object to be passed and have a registry for values of 64 | interest we want to pull from context. A good example of an item we want to 65 | automatically include in log lines is the `x-request-id` so we can tie log 66 | lines produced in the request path together. 67 | 68 | - Allow each component to have their own "scope". 69 | 70 | This allows us to control per component which levels of log lines we want 71 | to output at runtime. The interface design allows for this to be 72 | implemented without having an opinion on it. By providing at each library 73 | or component entry point the ability to provide a Logger implementation, 74 | this can be easily achieved. 75 | 76 | - Zero dependencies. 77 | 78 | Look at that lovely very empty go.mod and non-existent go.sum file. 79 | */ 80 | package telemetry 81 | -------------------------------------------------------------------------------- /group/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package group_test 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "strconv" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/tetratelabs/log" 25 | "github.com/tetratelabs/run" 26 | 27 | "github.com/tetratelabs/telemetry" 28 | "github.com/tetratelabs/telemetry/group" 29 | "github.com/tetratelabs/telemetry/scope" 30 | ) 31 | 32 | func TestService(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | expectedLines []string 36 | run func(l telemetry.Logger) 37 | }{ 38 | { 39 | // We use test.name to initialize level. 40 | "info", 41 | []string{ 42 | " info test v0.0.0-unofficial started [scope=\"test-info\"]", 43 | " info ok [scope=\"test-info\"]", 44 | " info haha [scope=\"test-info\"]", 45 | }, 46 | func(l telemetry.Logger) { 47 | l.Info("ok") 48 | l.Info("haha") 49 | }, 50 | }, 51 | { 52 | "debug", 53 | []string{ 54 | " info test v0.0.0-unofficial started [scope=\"test-debug\"]", 55 | " debug ok [scope=\"test-debug\"]", 56 | " debug haha [scope=\"test-debug\"]", 57 | }, 58 | func(l telemetry.Logger) { 59 | l.Debug("ok") 60 | l.Debug("haha") 61 | }, 62 | }, 63 | } 64 | 65 | scopeName := func(n string) string { 66 | return "test-" + n 67 | } 68 | 69 | // Register all possible scopes. Since UseLogger will register all possible scopes and can't be 70 | // changed. 71 | for _, test := range tests { 72 | scope.Register(scopeName(test.name), test.name) 73 | } 74 | 75 | tmp, err := ioutil.TempFile("", "log_test") 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | oldStdout := os.Stdout 80 | // Redirect stdout to tmp. 81 | os.Stdout = tmp 82 | defer func() { 83 | _ = os.Remove(tmp.Name()) 84 | os.Stdout = oldStdout 85 | }() 86 | 87 | defaultLogger := log.NewUnstructured() 88 | for _, test := range tests { 89 | var ( 90 | s, _ = scope.Find(scopeName(test.name)) 91 | g = &run.Group{Name: "test", Logger: s} 92 | svc = group.New(defaultLogger) 93 | ) 94 | g.Register(svc) 95 | 96 | oldArgs := os.Args 97 | // Set current scope output level. 98 | os.Args = []string{"cmd", "--log-output-level=" + scopeName(test.name) + ":" + test.name} 99 | 100 | if err := g.RunConfig(); err != nil { 101 | t.Fatalf("configuring run.Group: %v", err) 102 | } 103 | 104 | test.run(s) 105 | 106 | content, _ := os.ReadFile(tmp.Name()) 107 | lines := strings.Split(string(content), "\n") 108 | for i, expectedLine := range test.expectedLines { 109 | t.Run(test.name+strconv.Itoa(i), func(t *testing.T) { 110 | entries := strings.SplitN(lines[i], " ", 3) 111 | entry := entries[len(entries)-1] 112 | if entry != expectedLine { 113 | t.Errorf("got '%s', expecting to equal '%s'", entry, expectedLine) 114 | } 115 | }) 116 | } 117 | // Clear the content of the current temporary file. 118 | _ = os.Truncate(tmp.Name(), 0) 119 | os.Args = oldArgs 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /group/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package group provides a tetratelabs/run Group compatible scoped Logger 16 | // configuration handler. 17 | package group 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/tetratelabs/multierror" 24 | "github.com/tetratelabs/run" 25 | 26 | "github.com/tetratelabs/telemetry" 27 | "github.com/tetratelabs/telemetry/scope" 28 | ) 29 | 30 | const ( 31 | // LogOutputLevel is the name of the flag used to configure the logging levels. 32 | LogOutputLevel = "log-output-level" 33 | ) 34 | 35 | const ( 36 | // DefaultLogOutputLevel is the default level that will be configured for the loggers. 37 | DefaultLogOutputLevel = "info" 38 | ) 39 | 40 | type service struct { 41 | outputLevels string 42 | } 43 | 44 | // New returns a new run Group Config to manage configuration of our scoped 45 | // logger. 46 | func New(l telemetry.Logger) run.Config { 47 | scope.UseLogger(l) 48 | return &service{} 49 | } 50 | 51 | // Name implements run.Unit. 52 | func (s service) Name() string { 53 | return "log-manager" 54 | } 55 | 56 | // FlagSet implements run.Config. 57 | func (s *service) FlagSet() *run.FlagSet { 58 | if s.outputLevels == "" { 59 | s.outputLevels = scope.DefaultLevel().String() 60 | if s.outputLevels == "" { 61 | s.outputLevels = DefaultLogOutputLevel 62 | } 63 | } 64 | fs := run.NewFlagSet("Logging options") 65 | fs.StringVar(&s.outputLevels, LogOutputLevel, s.outputLevels, fmt.Sprintf( 66 | "Comma-separated minimum per-scope logging level of messages to output, "+ 67 | "in the form of [default_level,]:,:,... "+ 68 | "where scope can be one of [%s] and default_level or level can be "+ 69 | "one of [%s]", 70 | strings.Join(scope.Names(), ", "), 71 | strings.Join([]string{"debug", "info", "error", "none"}, ", "), 72 | )) 73 | 74 | return fs 75 | } 76 | 77 | // Validate implements run.Config. 78 | func (s *service) Validate() error { 79 | var mErr error 80 | 81 | s.outputLevels = strings.ToLower(s.outputLevels) 82 | outputLevels := strings.Split(s.outputLevels, ",") 83 | if len(outputLevels) == 0 { 84 | return nil 85 | } 86 | for _, ol := range outputLevels { 87 | osl := strings.Split(ol, ":") 88 | switch len(osl) { 89 | case 1: 90 | lvl, ok := telemetry.FromLevel(strings.Trim(ol, "\r\n\t ")) 91 | if !ok { 92 | mErr = multierror.Append(mErr, fmt.Errorf("%q is not a valid log level", ol)) 93 | continue 94 | } 95 | scope.SetAllScopes(lvl) 96 | case 2: 97 | lvl, ok := telemetry.FromLevel(strings.Trim(osl[1], "\r\n\t ")) 98 | if !ok { 99 | mErr = multierror.Append(mErr, fmt.Errorf("%q is not a valid log level", ol)) 100 | continue 101 | } 102 | if s, found := scope.Find(osl[0]); found { 103 | s.SetLevel(lvl) 104 | } else { 105 | mErr = multierror.Append(mErr, fmt.Errorf("%q is not a registered scope", osl[0])) 106 | } 107 | default: 108 | mErr = multierror.Append(mErr, fmt.Errorf("%q is not a valid : pair", ol)) 109 | } 110 | } 111 | 112 | return mErr 113 | } 114 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import "context" 18 | 19 | // Logger provides a simple yet powerful logging abstraction. 20 | type Logger interface { 21 | // Debug logging with key-value pairs. Don't be shy, use it. 22 | Debug(msg string, keyValuePairs ...interface{}) 23 | 24 | // Info logging with key-value pairs. This is for informational, but not 25 | // directly actionable conditions. It is highly recommended you attach a 26 | // Metric to these types of messages. Where a single informational or 27 | // warning style message might not be reason for action, a change in 28 | // occurrence does warrant action. By attaching a Metric for these logging 29 | // situations, you make this easy through histograms, thresholds, etc. 30 | Info(msg string, keyValuePairs ...interface{}) 31 | 32 | // Error logging with key-value pairs. Use this when application state and 33 | // stability are at risk. These types of conditions are actionable and often 34 | // alerted on. It is very strongly encouraged to add a Metric to each of 35 | // these types of messages. Metrics provide the easiest way to coordinate 36 | // processing of these concerns and triggering alerting systems through your 37 | // metrics backend. 38 | Error(msg string, err error, keyValuePairs ...interface{}) 39 | 40 | // SetLevel provides the ability to set the desired logging level. 41 | // This function can be used at runtime and must be safe for concurrent use. 42 | // 43 | // Note for Logger implementations, When creating a new Logger with the 44 | // With, Context, or Metric methods, the level should be set-able for all 45 | // from any of the Loggers sharing the same root Logger. 46 | SetLevel(lvl Level) 47 | 48 | // Level returns the currently configured logging level. 49 | Level() Level 50 | 51 | // With returns a new Logger decorated with the provided key-value pairs. 52 | With(keyValuePairs ...interface{}) Logger 53 | 54 | // Context returns a new Logger having access to Context for inclusion of 55 | // registered key-value pairs found in Context. If a Metric is also attached 56 | // to the Logger, the Metric LabelValue directives found in Context will 57 | // also be processed. 58 | Context(ctx context.Context) Logger 59 | 60 | // Metric returns a new Logger which will emit a measurement for the 61 | // provided Metric when the Log level is either Info or Error. 62 | // **Note** that in the event the Logger is set to only output Error level 63 | // messages, Info messages even though silenced from a logging perspective, 64 | // will still emit their Metric measurements. 65 | Metric(m Metric) Logger 66 | 67 | // Clone returns a new Logger based on the original implementation. 68 | Clone() Logger 69 | } 70 | 71 | // KeyValuesToContext takes provided Context, retrieves the already stored 72 | // key-value pairs from it, appends the in this function provided key-value 73 | // pairs and stores the result in the returned Context. 74 | func KeyValuesToContext(ctx context.Context, keyValuePairs ...interface{}) context.Context { 75 | if len(keyValuePairs) == 0 { 76 | return ctx 77 | } 78 | if len(keyValuePairs)%2 != 0 { 79 | keyValuePairs = append(keyValuePairs, "(MISSING)") 80 | } 81 | args := KeyValuesFromContext(ctx) 82 | args = append(args, keyValuePairs...) 83 | return context.WithValue(ctx, ctxKVP, args) 84 | } 85 | 86 | // KeyValuesFromContext retrieves key-value pairs that might be stored in the 87 | // provided Context. Logging implementations must use this function to retrieve 88 | // the key-value pairs they need to include if a Context object was attached to 89 | // them. 90 | func KeyValuesFromContext(ctx context.Context) (keyValuePairs []interface{}) { 91 | keyValuePairs, _ = ctx.Value(ctxKVP).([]interface{}) 92 | return 93 | } 94 | 95 | // RemoveKeyValuesFromContext returns a Context that copies the provided Context 96 | // but removes the key-value pairs that were stored. 97 | func RemoveKeyValuesFromContext(ctx context.Context) context.Context { 98 | return context.WithValue(ctx, ctxKVP, nil) 99 | } 100 | 101 | type tCtxKVP string 102 | 103 | var ctxKVP tCtxKVP 104 | -------------------------------------------------------------------------------- /function/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package function 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "testing" 24 | 25 | "github.com/tetratelabs/telemetry" 26 | ) 27 | 28 | func TestLogger(t *testing.T) { 29 | emitter := func(w io.Writer) Emit { 30 | return func(level telemetry.Level, msg string, err error, values Values) { 31 | _, _ = fmt.Fprintf(w, "level=%v msg=%q", level, msg) 32 | if err != nil { 33 | _, _ = fmt.Fprintf(w, " err=%v", err) 34 | } 35 | 36 | all := append(values.FromContext, values.FromLogger...) 37 | all = append(all, values.FromMethod...) 38 | _, _ = fmt.Fprintf(w, " %v", all) 39 | } 40 | } 41 | 42 | tests := []struct { 43 | name string 44 | level telemetry.Level 45 | logfunc func(telemetry.Logger) 46 | expected string 47 | metricCount float64 48 | }{ 49 | {"none", telemetry.LevelNone, func(l telemetry.Logger) { l.Error("text", errors.New("error")) }, "", 1}, 50 | {"disabled-info", telemetry.LevelNone, func(l telemetry.Logger) { l.Info("text") }, "", 1}, 51 | {"disabled-debug", telemetry.LevelNone, func(l telemetry.Logger) { l.Debug("text") }, "", 0}, 52 | {"disabled-error", telemetry.LevelNone, func(l telemetry.Logger) { l.Error("text", errors.New("error")) }, "", 1}, 53 | {"info", telemetry.LevelInfo, func(l telemetry.Logger) { l.Info("text") }, 54 | `level=info msg="text" [ctx value lvl info missing (MISSING)]`, 1}, 55 | {"info-with-values", telemetry.LevelInfo, func(l telemetry.Logger) { l.Info("text", "where", "there", 1, "1") }, 56 | `level=info msg="text" [ctx value lvl info missing (MISSING) where there 1 1]`, 1}, 57 | {"error", telemetry.LevelInfo, func(l telemetry.Logger) { l.Error("text", errors.New("error")) }, 58 | `level=error msg="text" err=error [ctx value lvl info missing (MISSING)]`, 1}, 59 | {"error-with-values", telemetry.LevelInfo, func(l telemetry.Logger) { l.Error("text", errors.New("error"), "where", "there", 1, "1") }, 60 | `level=error msg="text" err=error [ctx value lvl info missing (MISSING) where there 1 1]`, 1}, 61 | {"debug", telemetry.LevelDebug, func(l telemetry.Logger) { l.Debug("text") }, 62 | `level=debug msg="text" [ctx value lvl info missing (MISSING)]`, 0}, 63 | {"debug-with-values", telemetry.LevelDebug, func(l telemetry.Logger) { l.Debug("text", "where", "there", 1, "1") }, 64 | `level=debug msg="text" [ctx value lvl info missing (MISSING) where there 1 1]`, 0}, 65 | } 66 | 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | var out bytes.Buffer 70 | logger := NewLogger(emitter(&out)) 71 | 72 | logger.SetLevel(tt.level) 73 | if logger.Level() != tt.level { 74 | t.Fatalf("loger.Level()=%s, want: %s", logger.Level(), tt.level) 75 | } 76 | 77 | metric := mockMetric{} 78 | ctx := telemetry.KeyValuesToContext(context.Background(), "ctx", "value") 79 | l := logger.Context(ctx).Metric(&metric).With().With(1, "").With("lvl", telemetry.LevelInfo).With("missing") 80 | 81 | tt.logfunc(l) 82 | 83 | str := out.String() 84 | if str != tt.expected { 85 | t.Fatalf("expected %s to match %s", str, tt.expected) 86 | } 87 | if metric.count != tt.metricCount { 88 | t.Fatalf("metric.count=%v, want %v", metric.count, tt.metricCount) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func TestSetUnexpectedLevel(t *testing.T) { 95 | logger := NewLogger(nil) 96 | withvalues := logger.With("key", "value") 97 | logger.SetLevel(telemetry.LevelInfo - 1) 98 | 99 | if withvalues.Level() != telemetry.LevelError { 100 | t.Fatalf("Logger.Level()=%v, want: %v", withvalues.Level(), telemetry.LevelError) 101 | } 102 | } 103 | 104 | func TestClone(t *testing.T) { 105 | logger := NewLogger(nil) 106 | 107 | // Enhancing a logger with values does not alter the logger itself, and setting hte level should 108 | // affect the enhanced logger and the original one. We're not altering the 'scope' here. 109 | withvalues := logger.With("key", "value").Context(context.Background()).Metric(nil) 110 | 111 | // Cloning a logger returns a new independent one, with the logging level detached from the original 112 | cloned := withvalues.Clone() 113 | 114 | // Verify that the level is properly set. The level should affect both, as it is the same logger 115 | // with just additional info; it is not a clone. 116 | logger.SetLevel(telemetry.LevelDebug) 117 | 118 | if logger.Level() != telemetry.LevelDebug { 119 | t.Fatalf("logger.Level()=%v, want: %v", logger.Level(), telemetry.LevelDebug) 120 | } 121 | if withvalues.Level() != telemetry.LevelDebug { 122 | t.Fatalf("withvalues.Level()=%v, want: %v", withvalues.Level(), telemetry.LevelDebug) 123 | } 124 | if cloned.Level() != telemetry.LevelInfo { 125 | t.Fatalf("cloned.Level()=%v, want: %v", cloned.Level(), telemetry.LevelInfo) 126 | } 127 | 128 | withvalues.SetLevel(telemetry.LevelNone) 129 | 130 | if logger.Level() != telemetry.LevelNone { 131 | t.Fatalf("logger.Level()=%v, want: %v", logger.Level(), telemetry.LevelNone) 132 | } 133 | if withvalues.Level() != telemetry.LevelNone { 134 | t.Fatalf("withvalues.Level()=%v, want: %v", withvalues.Level(), telemetry.LevelNone) 135 | } 136 | if cloned.Level() != telemetry.LevelInfo { 137 | t.Fatalf("cloned.Level()=%v, want: %v", cloned.Level(), telemetry.LevelInfo) 138 | } 139 | 140 | cloned.SetLevel(telemetry.LevelError) 141 | 142 | if logger.Level() != telemetry.LevelNone { 143 | t.Fatalf("logger.Level()=%v, want: %v", logger.Level(), telemetry.LevelNone) 144 | } 145 | if withvalues.Level() != telemetry.LevelNone { 146 | t.Fatalf("withvalues.Level()=%v, want: %v", withvalues.Level(), telemetry.LevelNone) 147 | } 148 | if cloned.Level() != telemetry.LevelError { 149 | t.Fatalf("cloned.Level()=%v, want: %v", cloned.Level(), telemetry.LevelError) 150 | } 151 | } 152 | 153 | type mockMetric struct { 154 | telemetry.Metric 155 | count float64 156 | } 157 | 158 | func (m *mockMetric) RecordContext(_ context.Context, value float64) { m.count += value } 159 | -------------------------------------------------------------------------------- /scope/scope_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package scope 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "strconv" 24 | "sync" 25 | "testing" 26 | 27 | "github.com/tetratelabs/telemetry" 28 | "github.com/tetratelabs/telemetry/function" 29 | ) 30 | 31 | func TestParallel(t *testing.T) { 32 | logger := Register("root", "root scope") 33 | ctx := context.Background() 34 | wg := sync.WaitGroup{} 35 | routines := 100 36 | wg.Add(4 * routines) 37 | m := &mockMetric{} 38 | for i := 0; i < routines; i++ { 39 | scopeName := strconv.Itoa(i - i%2) 40 | go func() { 41 | l := logger.Context(ctx).Metric(m).With("key", "value") 42 | l.Debug("test log line") 43 | wg.Done() 44 | }() 45 | go func() { 46 | Register(scopeName, "test scope") 47 | wg.Done() 48 | }() 49 | go func() { 50 | lvl := DefaultLevel() 51 | logger.Debug(strconv.Itoa(int(lvl))) 52 | wg.Done() 53 | }() 54 | go func() { 55 | SetDefaultLevel(telemetry.LevelInfo) 56 | wg.Done() 57 | }() 58 | } 59 | wg.Wait() 60 | cleanup() 61 | } 62 | 63 | func TestLogger(t *testing.T) { 64 | emitter := func(w io.Writer) function.Emit { 65 | return func(level telemetry.Level, msg string, err error, values function.Values) { 66 | _, _ = fmt.Fprintf(w, "level=%v msg=%q", level, msg) 67 | if err != nil { 68 | _, _ = fmt.Fprintf(w, " err=%v", err) 69 | } 70 | 71 | all := append(values.FromContext, values.FromLogger...) 72 | all = append(all, values.FromMethod...) 73 | _, _ = fmt.Fprintf(w, " %v", all) 74 | } 75 | } 76 | 77 | tests := []struct { 78 | name string 79 | level telemetry.Level 80 | logfunc func(telemetry.Logger) 81 | expected string 82 | metricCount float64 83 | }{ 84 | {"none", telemetry.LevelNone, func(l telemetry.Logger) { l.Error("text", errors.New("error")) }, "", 1}, 85 | {"disabled-info", telemetry.LevelNone, func(l telemetry.Logger) { l.Info("text") }, "", 1}, 86 | {"disabled-debug", telemetry.LevelNone, func(l telemetry.Logger) { l.Debug("text") }, "", 0}, 87 | {"disabled-error", telemetry.LevelNone, func(l telemetry.Logger) { l.Error("text", errors.New("error")) }, "", 1}, 88 | {"info", telemetry.LevelInfo, func(l telemetry.Logger) { l.Info("text") }, 89 | `level=info msg="text" [ctx value scope info lvl info missing (MISSING)]`, 1}, 90 | {"info-with-values", telemetry.LevelInfo, func(l telemetry.Logger) { l.Info("text", "where", "there", 1, "1") }, 91 | `level=info msg="text" [ctx value scope info-with-values lvl info missing (MISSING) where there 1 1]`, 1}, 92 | {"error", telemetry.LevelInfo, func(l telemetry.Logger) { l.Error("text", errors.New("error")) }, 93 | `level=error msg="text" err=error [ctx value scope error lvl info missing (MISSING)]`, 1}, 94 | {"error-with-values", telemetry.LevelInfo, func(l telemetry.Logger) { l.Error("text", errors.New("error"), "where", "there", 1, "1") }, 95 | `level=error msg="text" err=error [ctx value scope error-with-values lvl info missing (MISSING) where there 1 1]`, 1}, 96 | {"debug", telemetry.LevelDebug, func(l telemetry.Logger) { l.Debug("text") }, 97 | `level=debug msg="text" [ctx value scope debug lvl info missing (MISSING)]`, 0}, 98 | {"debug-with-values", telemetry.LevelDebug, func(l telemetry.Logger) { l.Debug("text", "where", "there", 1, "1") }, 99 | `level=debug msg="text" [ctx value scope debug-with-values lvl info missing (MISSING) where there 1 1]`, 0}, 100 | } 101 | 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | t.Cleanup(cleanup) 105 | 106 | var out bytes.Buffer 107 | UseLogger(function.NewLogger(emitter(&out))) 108 | 109 | _ = Register(tt.name, "test logger") 110 | logger, _ := Find(tt.name) 111 | 112 | logger.SetLevel(tt.level) 113 | if logger.Level() != tt.level { 114 | t.Fatalf("loger.Level()=%s, want: %s", logger.Level(), tt.level) 115 | } 116 | 117 | metric := mockMetric{} 118 | ctx := telemetry.KeyValuesToContext(context.Background(), "ctx", "value") 119 | l := logger.Context(ctx).Metric(&metric).With().With(1, "").With("lvl", telemetry.LevelInfo).With("missing") 120 | 121 | tt.logfunc(l) 122 | 123 | str := out.String() 124 | if str != tt.expected { 125 | t.Fatalf("expected %s to match %s", str, tt.expected) 126 | } 127 | if metric.count != tt.metricCount { 128 | t.Fatalf("metric.count=%v, want %v", metric.count, tt.metricCount) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | func TestSetLevel(t *testing.T) { 135 | t.Cleanup(cleanup) 136 | 137 | logger := Register("test-set-level", "test logger") 138 | 139 | withvalues := logger.With("key", "value") 140 | logger.SetLevel(telemetry.LevelInfo - 1) 141 | 142 | if withvalues.Level() != telemetry.LevelError { 143 | t.Fatalf("logger.Level()=%v, want: %v", withvalues.Level(), telemetry.LevelError) 144 | } 145 | } 146 | 147 | func TestTwoScopes(t *testing.T) { 148 | scopeA := Register("a", "Messages from a") 149 | scopeB := Register("b", "Messages from b") 150 | UseLogger(function.NewLogger(func(level telemetry.Level, msg string, err error, values function.Values) { 151 | // Do nothing 152 | })) 153 | 154 | scopeA.SetLevel(telemetry.LevelDebug) 155 | scopeB.SetLevel(telemetry.LevelDebug) 156 | 157 | if scopeA.Level() != telemetry.LevelDebug || scopeB.Level() != telemetry.LevelDebug { 158 | t.Fatalf("logger.Level=%s / logger2.Level=%s, want: %s/%s", 159 | scopeA.Level().String(), scopeB.Level().String(), telemetry.LevelDebug, telemetry.LevelDebug) 160 | } 161 | 162 | scopeA.SetLevel(telemetry.LevelInfo) 163 | if scopeA.Level() != telemetry.LevelInfo || scopeB.Level() != telemetry.LevelDebug { 164 | t.Fatalf("logger.Level=%s / logger2.Level=%s, want: %s/%s", 165 | scopeA.Level().String(), scopeB.Level().String(), telemetry.LevelInfo, telemetry.LevelDebug) 166 | } 167 | } 168 | 169 | func TestFind(t *testing.T) { 170 | s, ok := Find("unexisting") 171 | if ok { 172 | t.Fatalf("expected Find to have returned nil, got: %v", s) 173 | } 174 | } 175 | 176 | func cleanup() { 177 | scopes = make(map[string]*scope) 178 | uninitialized = make(map[string][]*scope) 179 | defaultLogger = nil 180 | } 181 | 182 | type mockMetric struct { 183 | telemetry.Metric 184 | count float64 185 | } 186 | 187 | func (m *mockMetric) RecordContext(_ context.Context, value float64) { m.count += value } 188 | -------------------------------------------------------------------------------- /metric.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package telemetry 16 | 17 | import "context" 18 | 19 | // Unit encodes the standard name for describing the quantity measured by a 20 | // Metric (if applicable). 21 | type Unit string 22 | 23 | // Predefined units for use with the monitoring package. 24 | const ( 25 | None Unit = "1" 26 | Bytes Unit = "By" 27 | Seconds Unit = "s" 28 | Milliseconds Unit = "ms" 29 | ) 30 | 31 | // Metric collects numerical observations. 32 | type Metric interface { 33 | // Increment records a value of 1 for the current Metric. 34 | // For Sums, this is equivalent to adding 1 to the current value. 35 | // For Gauges, this is equivalent to setting the value to 1. 36 | // For Distributions, this is equivalent to making an observation of value 1. 37 | Increment() 38 | 39 | // Decrement records a value of -1 for the current Metric. 40 | // For Sums, this is equivalent to subtracting -1 to the current value. 41 | // For Gauges, this is equivalent to setting the value to -1. 42 | // For Distributions, this is equivalent to making an observation of value -1. 43 | Decrement() 44 | 45 | // Name returns the name value of a Metric. 46 | Name() string 47 | 48 | // Record makes an observation of the provided value for the given Metric. 49 | // LabelValues added through With will be processed in sequence. 50 | Record(value float64) 51 | 52 | // RecordContext makes an observation of the provided value for the given 53 | // Metric. 54 | // If LabelValues for registered Labels are found in context, they will be 55 | // processed in sequence, after which the LabelValues added through With 56 | // are handled. 57 | RecordContext(ctx context.Context, value float64) 58 | 59 | // With returns the Metric with the provided LabelValues encapsulated. This 60 | // allows creating a set of pre-dimensioned data for recording purposes. 61 | // It also allows a way to clear out LabelValues found in an attached 62 | // Context if needing to sanitize. 63 | With(labelValues ...LabelValue) Metric 64 | } 65 | 66 | // DerivedMetric can be used to supply values that dynamically derive from internal 67 | // state, but are not updated based on any specific event. Their value will be calculated 68 | // based on a value func that executes when the metrics are exported. 69 | // 70 | // At the moment, only a Gauge type is supported. 71 | type DerivedMetric interface { 72 | // Name returns the name value of a DerivedMetric. 73 | Name() string 74 | 75 | // ValueFrom is used to update the derived value with the provided 76 | // function and the associated label values. If the metric is unlabeled, 77 | // ValueFrom may be called without any labelValues. Otherwise, the labelValues 78 | // supplied MUST match the label keys supplied at creation time both in number 79 | // and in order. 80 | ValueFrom(valueFn func() float64, labelValues ...LabelValue) DerivedMetric 81 | } 82 | 83 | // LabelValue holds an action to take on a metric dimension's value. 84 | type LabelValue interface{} 85 | 86 | // Label holds a metric dimension which can be operated on using the interface 87 | // methods. 88 | type Label interface { 89 | // Insert will insert the provided value for the Label if not set. 90 | Insert(value string) LabelValue 91 | 92 | // Update will update the Label with provided value if already set. 93 | Update(value string) LabelValue 94 | 95 | // Upsert will insert or replace the provided value for the Label. 96 | Upsert(value string) LabelValue 97 | 98 | // Delete will remove the Label's value. 99 | Delete() LabelValue 100 | } 101 | 102 | // MetricSink bridges libraries bootstrapping metrics from metrics 103 | // instrumentation implementations. 104 | type MetricSink interface { 105 | // NewSum intents to create a new Metric with an aggregation type of Sum 106 | // (the values will be cumulative). That means that data collected by the 107 | // new Metric will be summed before export. 108 | NewSum(name, description string, opts ...MetricOption) Metric 109 | 110 | // NewGauge intents to creates a new Metric with an aggregation type of 111 | // LastValue. That means that data collected by the new Metric will export 112 | // only the last recorded value. 113 | NewGauge(name, description string, opts ...MetricOption) Metric 114 | 115 | // NewDistribution intents to create a new Metric with an aggregation type 116 | // of Distribution. This means that the data collected by the Metric will be 117 | // collected and exported as a histogram, with the specified bounds. 118 | NewDistribution(name, description string, bounds []float64, opts ...MetricOption) Metric 119 | 120 | // NewLabel creates a new Label to be used as a metrics dimension. 121 | NewLabel(name string) Label 122 | 123 | // ContextWithLabels takes the existing LabelValue collection found in 124 | // Context and appends the Label operations as received from the provided 125 | // values on top, which is then added to the returned Context. The function 126 | // can return an error in case the provided values contain invalid label 127 | // names. 128 | ContextWithLabels(ctx context.Context, values ...LabelValue) (context.Context, error) 129 | } 130 | 131 | // DerivedMetricSink bridges libraries bootstrapping metrics from metrics 132 | // instrumentation implementations. 133 | type DerivedMetricSink interface { 134 | // NewDerivedGauge intents to create a new Metric with an aggregation type 135 | // of LastValue. That means that data collected by the new Metric will 136 | // export only the last recorded value. 137 | // Unlike NewGauge, the DerivedGauge accepts functions which are called to 138 | // get the current value. 139 | NewDerivedGauge(name, description string) DerivedMetric 140 | } 141 | 142 | // MetricOption implements a functional option type for our Metrics. 143 | type MetricOption func(*MetricOptions) 144 | 145 | // MetricOptions hold commonly used but optional Metric configuration. 146 | type MetricOptions struct { 147 | // EnabledCondition can hold a function which decides if metric is enabled. 148 | EnabledCondition func() bool 149 | // Unit holds the unit specifier of a Metric. 150 | Unit Unit 151 | // Labels holds the registered dimensions for the Metric. 152 | Labels []Label 153 | } 154 | 155 | // WithLabels provides a configuration MetricOption for a new Metric, providing 156 | // the required dimensions for data collection of that Metric. 157 | func WithLabels(labels ...Label) MetricOption { 158 | return func(opts *MetricOptions) { 159 | opts.Labels = labels 160 | } 161 | } 162 | 163 | // WithUnit provides a configuration MetricOption for a new Metric, providing 164 | // Unit of measure information for a new Metric. 165 | func WithUnit(unit Unit) MetricOption { 166 | return func(opts *MetricOptions) { 167 | opts.Unit = unit 168 | } 169 | } 170 | 171 | // WithEnabled allows a metric to be conditionally enabled if the provided 172 | // function returns true. 173 | // If disabled, metric operations will do nothing. 174 | func WithEnabled(enabled func() bool) MetricOption { 175 | return func(opts *MetricOptions) { 176 | opts.EnabledCondition = enabled 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /function/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package function provides an implementation of the telemetry.Logger interface 16 | // that uses a given function to emit logs. 17 | package function 18 | 19 | import ( 20 | "context" 21 | "sync/atomic" 22 | 23 | "github.com/tetratelabs/telemetry" 24 | ) 25 | 26 | type ( 27 | // Emit is a function that will be used to produce log messages by the function Logger. 28 | // Implementations of this function just need to implement the log writing. Decisions on whether to 29 | // emit a log or not based on the log level should not be made here as the function Logger already 30 | // takes care of that. 31 | // Similarly, the keyValues parameter presented in this method will already contain al the key/value pairs 32 | // that need to be logged. 33 | // The function will only be called when the log actually needs to be emitted. 34 | Emit func(level telemetry.Level, msg string, err error, values Values) 35 | 36 | // Values contains all the key/value pairs to be included when emitting logs. 37 | Values struct { 38 | // FromContext has all the key/value pairs that have been added to the Logger Context 39 | FromContext []interface{} 40 | // FromLogger has all the key/value pairs that have been added to the Logger object itself 41 | FromLogger []interface{} 42 | // FromMethod has the key/value pairs that were passed to the logging method. 43 | FromMethod []interface{} 44 | } 45 | 46 | // Logger is an implementation of the telemetry.Logger that allows configuring named 47 | // loggers that can be configured independently and referenced by name. 48 | Logger struct { 49 | // ctx holds the Context to extract key-value pairs from to be added to each 50 | // log line. 51 | ctx context.Context 52 | // args holds the key-value pairs to be added to each log line. 53 | args []interface{} 54 | // metric holds the Metric to increment each time Info() or Error() is called. 55 | metric telemetry.Metric 56 | // level holds the configured log level. 57 | level *int32 58 | // emitFunc is the function that will be used to actually emit the logs 59 | emitFunc Emit 60 | } 61 | ) 62 | 63 | // compile time check for compatibility with the telemetry.Logger interface. 64 | var _ telemetry.Logger = (*Logger)(nil) 65 | 66 | // NewLogger creates a new function Logger that uses the given Emit function to write log messages. 67 | // Loggers are configured at telemetry.LevelInfo level by default. 68 | func NewLogger(emitFunc Emit) telemetry.Logger { 69 | lvl := int32(telemetry.LevelInfo) 70 | return &Logger{ 71 | ctx: context.Background(), 72 | level: &lvl, 73 | emitFunc: emitFunc, 74 | } 75 | } 76 | 77 | // Debug emits a log message at debug level with the given key value pairs. 78 | func (l *Logger) Debug(msg string, keyValues ...interface{}) { 79 | if !l.enabled(telemetry.LevelDebug) { 80 | return 81 | } 82 | l.emit(telemetry.LevelDebug, msg, nil, keyValues) 83 | } 84 | 85 | // Info emits a log message at info level with the given key value pairs. 86 | func (l *Logger) Info(msg string, keyValues ...interface{}) { 87 | // even if we don't output the log line due to the level configuration, 88 | // we always emit the Metric if it is set. 89 | if l.metric != nil { 90 | l.metric.RecordContext(l.ctx, 1) 91 | } 92 | if !l.enabled(telemetry.LevelInfo) { 93 | return 94 | } 95 | l.emit(telemetry.LevelInfo, msg, nil, keyValues) 96 | } 97 | 98 | // Error emits a log message at error level with the given key value pairs. 99 | // The given error will be used as the last parameter in the message format 100 | // string. 101 | func (l *Logger) Error(msg string, err error, keyValues ...interface{}) { 102 | // even if we don't output the log line due to the level configuration, 103 | // we always emit the Metric if it is set. 104 | if l.metric != nil { 105 | l.metric.RecordContext(l.ctx, 1) 106 | } 107 | 108 | if !l.enabled(telemetry.LevelError) { 109 | return 110 | } 111 | 112 | l.emit(telemetry.LevelError, msg, err, keyValues) 113 | } 114 | 115 | // emit the given log with all the key/values that have been accumulated. 116 | func (l *Logger) emit(level telemetry.Level, msg string, err error, keyValues []interface{}) { 117 | // Note that here we don't ensure an even number of arguments in the keyValues slice. 118 | // We let that to the emit function implementation with the idea of being able to accommodate 119 | // unstructured loggers that don't use arguments as key/value pairs. 120 | l.emitFunc(level, msg, err, Values{ 121 | FromContext: telemetry.KeyValuesFromContext(l.ctx), 122 | FromLogger: l.args, 123 | FromMethod: keyValues, 124 | }) 125 | } 126 | 127 | // Level returns the logging level configured for this Logger. 128 | func (l *Logger) Level() telemetry.Level { return telemetry.Level(atomic.LoadInt32(l.level)) } 129 | 130 | // SetLevel configures the logging level for the Logger. 131 | func (l *Logger) SetLevel(level telemetry.Level) { 132 | switch { 133 | case level < telemetry.LevelError: 134 | level = telemetry.LevelNone 135 | case level < telemetry.LevelInfo: 136 | level = telemetry.LevelError 137 | case level < telemetry.LevelDebug: 138 | level = telemetry.LevelInfo 139 | default: 140 | level = telemetry.LevelDebug 141 | } 142 | 143 | atomic.StoreInt32(l.level, int32(level)) 144 | } 145 | 146 | // enabled checks if the current Logger should emit log messages for the given 147 | // logging level. 148 | func (l *Logger) enabled(level telemetry.Level) bool { return l.emitFunc != nil && level <= l.Level() } 149 | 150 | // With returns Logger with provided key value pairs attached. 151 | func (l *Logger) With(keyValues ...interface{}) telemetry.Logger { 152 | if len(keyValues) == 0 { 153 | return l 154 | } 155 | if len(keyValues)%2 != 0 { 156 | keyValues = append(keyValues, "(MISSING)") 157 | } 158 | 159 | // We don't call Clone() here as we don't want to deference the level pointer; 160 | // we just want to add the given args. 161 | newLogger := newLoggerWithValues(l.ctx, l.metric, l.level, l.emitFunc, l.args) 162 | 163 | for i := 0; i < len(keyValues); i += 2 { 164 | if k, ok := keyValues[i].(string); ok { 165 | newLogger.args = append(newLogger.args, k, keyValues[i+1]) 166 | } 167 | } 168 | 169 | return newLogger 170 | } 171 | 172 | // Context attaches provided Context to the Logger allowing metadata found in 173 | // this context to be used for log lines and metrics labels. 174 | func (l *Logger) Context(ctx context.Context) telemetry.Logger { 175 | // We don't call Clone() here as we don't want to deference the level pointer; 176 | // we just want to set the context. 177 | return newLoggerWithValues(ctx, l.metric, l.level, l.emitFunc, l.args) 178 | } 179 | 180 | // Metric attaches provided Metric to the Logger allowing this metric to 181 | // record each invocation of Info and Error log lines. If context is available 182 | // in the Logger, it can be used for Metrics labels. 183 | func (l *Logger) Metric(m telemetry.Metric) telemetry.Logger { 184 | // We don't call Clone() here as we don't want to deference the level pointer; 185 | // we just want to set the metric. 186 | return newLoggerWithValues(l.ctx, m, l.level, l.emitFunc, l.args) 187 | } 188 | 189 | // Clone the current Logger and return it 190 | func (l *Logger) Clone() telemetry.Logger { 191 | // When cloning the logger, we don't want both logger to share a level. 192 | // We need to dereference the pointer and set the level properly. 193 | lvl := *l.level 194 | return newLoggerWithValues(l.ctx, l.metric, &lvl, l.emitFunc, l.args) 195 | } 196 | 197 | // newLoggerWithValues creates a new instance of a logger with the given data. 198 | func newLoggerWithValues(ctx context.Context, m telemetry.Metric, l *int32, f Emit, args []interface{}) *Logger { 199 | newLogger := &Logger{ 200 | args: make([]interface{}, len(args)), 201 | ctx: ctx, 202 | metric: m, 203 | level: l, 204 | emitFunc: f, 205 | } 206 | copy(newLogger.args, args) 207 | return newLogger 208 | } 209 | -------------------------------------------------------------------------------- /scope/scope.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2023. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package scope provides a scoped logger facade for telemetry.Logger 16 | // implementations. 17 | package scope 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "sort" 23 | "strings" 24 | "sync" 25 | "sync/atomic" 26 | 27 | "github.com/tetratelabs/telemetry" 28 | ) 29 | 30 | var ( 31 | _ telemetry.Logger = (*scope)(nil) 32 | 33 | lock = sync.Mutex{} 34 | scopes = make(map[string]*scope) 35 | uninitialized = make(map[string][]*scope) 36 | defaultLogger telemetry.Logger 37 | 38 | // PanicOnUninitialized can be used when testing for sequencing issues 39 | // between creating log lines and initializing the actual logger 40 | // implementation to use. 41 | PanicOnUninitialized bool 42 | ) 43 | 44 | const ( 45 | // Key used to store the name of scope in the logger key/value pairs. 46 | Key = "scope" 47 | ) 48 | 49 | // Scope configures a named logger that can be configured independently. 50 | type Scope interface { 51 | telemetry.Logger 52 | 53 | // Name of the logging scope 54 | Name() string 55 | // Description of the logging scope 56 | Description() string 57 | } 58 | 59 | var _ Scope = (*scope)(nil) 60 | 61 | // scope provides scoped logging functionality. 62 | type scope struct { 63 | logger telemetry.Logger 64 | kvs []interface{} 65 | ctx context.Context 66 | metric telemetry.Metric 67 | name string 68 | description string 69 | level *int32 70 | } 71 | 72 | // Name of the logging scope 73 | func (s *scope) Name() string { 74 | return s.name 75 | } 76 | 77 | // Description of the logging scope 78 | func (s *scope) Description() string { 79 | return s.description 80 | } 81 | 82 | // Debug implements telemetry.Logger. 83 | func (s *scope) Debug(msg string, keyValuePairs ...interface{}) { 84 | if s.logger != nil { 85 | s.logger.Debug(msg, keyValuePairs...) 86 | } 87 | if PanicOnUninitialized { 88 | panic("calling Debug on uninitialized logger") 89 | } 90 | } 91 | 92 | // Info implements telemetry.Logger. 93 | func (s *scope) Info(msg string, keyValuePairs ...interface{}) { 94 | if s.logger != nil { 95 | s.logger.Info(msg, keyValuePairs...) 96 | } 97 | if PanicOnUninitialized { 98 | panic("calling Info on uninitialized logger") 99 | } 100 | } 101 | 102 | // Error implements telemetry.Logger. 103 | func (s *scope) Error(msg string, err error, keyValuePairs ...interface{}) { 104 | if s.logger != nil { 105 | s.logger.Error(msg, err, keyValuePairs...) 106 | } 107 | if PanicOnUninitialized { 108 | panic("calling Error on uninitialized logger") 109 | } 110 | } 111 | 112 | // With implements telemetry.Logger. 113 | func (s *scope) With(keyValuePairs ...interface{}) telemetry.Logger { 114 | if len(keyValuePairs) == 0 { 115 | return s 116 | } 117 | if len(keyValuePairs)%2 != 0 { 118 | keyValuePairs = append(keyValuePairs, "(MISSING)") 119 | } 120 | if s.logger != nil { 121 | return s.logger.With(keyValuePairs...) 122 | } 123 | sc := &scope{ 124 | name: s.name, 125 | description: s.description, 126 | kvs: make([]interface{}, len(s.kvs), len(s.kvs)+len(keyValuePairs)), 127 | ctx: s.ctx, 128 | metric: s.metric, 129 | level: s.level, 130 | } 131 | copy(sc.kvs, keyValuePairs) 132 | for i := 0; i < len(keyValuePairs); i += 2 { 133 | if k, ok := keyValuePairs[i].(string); ok { 134 | sc.kvs = append(sc.kvs, k, keyValuePairs[i+1]) 135 | } 136 | } 137 | lock.Lock() 138 | uninitialized[s.name] = append(uninitialized[s.name], sc) 139 | lock.Unlock() 140 | 141 | return sc 142 | } 143 | 144 | // Context implements telemetry.Logger. 145 | func (s *scope) Context(ctx context.Context) telemetry.Logger { 146 | if s.logger != nil { 147 | return s.logger.Context(ctx) 148 | } 149 | 150 | sc := s.Clone() 151 | sc.(*scope).ctx = ctx 152 | lock.Lock() 153 | uninitialized[s.name] = append(uninitialized[s.name], sc.(*scope)) 154 | lock.Unlock() 155 | return sc 156 | } 157 | 158 | // Metric implements telemetry.Logger. 159 | func (s *scope) Metric(m telemetry.Metric) telemetry.Logger { 160 | if s.logger != nil { 161 | return s.logger.Metric(m) 162 | } 163 | 164 | sc := s.Clone() 165 | sc.(*scope).metric = m 166 | lock.Lock() 167 | uninitialized[s.name] = append(uninitialized[s.name], sc.(*scope)) 168 | lock.Unlock() 169 | return sc 170 | } 171 | 172 | // Clone implements level.Logger. 173 | func (s *scope) Clone() telemetry.Logger { 174 | var logger telemetry.Logger 175 | if s.logger != nil { 176 | logger = s.logger.Clone() 177 | } 178 | 179 | scope := &scope{ 180 | logger: logger, 181 | name: s.name, 182 | description: s.description, 183 | kvs: make([]interface{}, len(s.kvs)), 184 | ctx: s.ctx, 185 | metric: s.metric, 186 | level: s.level, 187 | } 188 | 189 | copy(scope.kvs, s.kvs) 190 | 191 | return scope 192 | } 193 | 194 | // SetLevel implements level.Logger. 195 | func (s *scope) SetLevel(lvl telemetry.Level) { 196 | if s.logger != nil { 197 | s.logger.SetLevel(lvl) 198 | return 199 | } 200 | 201 | switch { 202 | case lvl < telemetry.LevelError: 203 | lvl = telemetry.LevelNone 204 | case lvl < telemetry.LevelInfo: 205 | lvl = telemetry.LevelError 206 | case lvl < telemetry.LevelDebug: 207 | lvl = telemetry.LevelInfo 208 | default: 209 | lvl = telemetry.LevelDebug 210 | } 211 | 212 | atomic.StoreInt32(s.level, int32(lvl)) 213 | } 214 | 215 | // Level implements level.Logger. 216 | func (s *scope) Level() telemetry.Level { 217 | if s.logger != nil { 218 | return s.logger.Level() 219 | } 220 | return telemetry.Level(atomic.LoadInt32(s.level)) 221 | } 222 | 223 | // Register a new scoped Logger. 224 | func Register(name, description string) Scope { 225 | if strings.ContainsAny(name, ":,.") { 226 | return nil 227 | } 228 | 229 | lock.Lock() 230 | defer lock.Unlock() 231 | 232 | name = strings.ToLower(strings.Trim(name, "\r\n\t ")) 233 | sc, ok := scopes[name] 234 | if !ok { 235 | level := int32(defaultLevel()) 236 | sc = &scope{ 237 | name: name, 238 | description: description, 239 | ctx: context.Background(), 240 | kvs: []interface{}{Key, name}, 241 | level: &level, 242 | } 243 | if defaultLogger != nil { 244 | sc.logger = defaultLogger.Clone().With(Key, name) 245 | } 246 | 247 | scopes[name] = sc 248 | } 249 | if defaultLogger == nil { 250 | uninitialized[name] = append(uninitialized[name], sc) 251 | } 252 | 253 | return sc 254 | } 255 | 256 | // Find a scoped logger by its name. 257 | func Find(name string) (Scope, bool) { 258 | name = strings.ToLower(strings.Trim(name, "\r\n\t ")) 259 | lock.Lock() 260 | s, ok := scopes[name] 261 | lock.Unlock() 262 | return s, ok 263 | } 264 | 265 | // List all registered Scopes 266 | func List() map[string]Scope { 267 | lock.Lock() 268 | defer lock.Unlock() 269 | 270 | sc := make(map[string]Scope, len(scopes)) 271 | for k, v := range scopes { 272 | sc[k] = v 273 | } 274 | 275 | return sc 276 | } 277 | 278 | // Names returns all registered scope names. 279 | func Names() []string { 280 | lock.Lock() 281 | defer lock.Unlock() 282 | 283 | s := make([]string, 0, len(scopes)) 284 | for k := range scopes { 285 | s = append(s, k) 286 | } 287 | 288 | return s 289 | } 290 | 291 | // PrintRegistered outputs a list of registered scopes with their log level on 292 | // stdout. 293 | func PrintRegistered() { 294 | lock.Lock() 295 | defer lock.Unlock() 296 | 297 | pad := 7 298 | names := make([]string, 0, len(scopes)) 299 | for n := range scopes { 300 | names = append(names, n) 301 | if len(n) > pad { 302 | pad = len(n) 303 | } 304 | } 305 | sort.Strings(names) 306 | 307 | fmt.Println("registered logging scopes:") 308 | fmt.Printf("- %-*s [%-5s] %s\n", 309 | pad, 310 | "default", 311 | defaultLevel().String(), 312 | "", 313 | ) 314 | for _, n := range names { 315 | sc := scopes[n] 316 | fmt.Printf("- %-*s [%-5s] %s\n", 317 | pad, 318 | sc.name, 319 | sc.Level().String(), 320 | sc.description, 321 | ) 322 | } 323 | } 324 | 325 | // SetAllScopes sets the logging level to all existing scopes and uses this 326 | // level for new scopes. 327 | func SetAllScopes(lvl telemetry.Level) { 328 | lock.Lock() 329 | defer lock.Unlock() 330 | 331 | if defaultLogger != nil { 332 | defaultLogger.SetLevel(lvl) 333 | for _, sc := range scopes { 334 | sc.SetLevel(lvl) 335 | } 336 | } 337 | } 338 | 339 | // SetDefaultLevel sets the default level used for new scopes. 340 | func SetDefaultLevel(lvl telemetry.Level) { 341 | lock.Lock() 342 | defer lock.Unlock() 343 | 344 | if defaultLogger != nil { 345 | defaultLogger.SetLevel(lvl) 346 | } 347 | } 348 | 349 | // DefaultLevel returns the logging level used for new scopes. 350 | func DefaultLevel() telemetry.Level { 351 | lock.Lock() 352 | defer lock.Unlock() 353 | 354 | return defaultLevel() 355 | } 356 | 357 | func defaultLevel() telemetry.Level { 358 | if defaultLogger != nil { 359 | return defaultLogger.Level() 360 | } 361 | return telemetry.LevelNone 362 | } 363 | 364 | // UseLogger takes a logger and updates already registered scopes to use it. 365 | // This function can only be used once. It can't override an already initialized 366 | // logger. Therefore, set this as soon as possible. 367 | func UseLogger(logger telemetry.Logger) { 368 | if logger == nil { 369 | return 370 | } 371 | 372 | lock.Lock() 373 | defer lock.Unlock() 374 | 375 | if defaultLogger != nil { 376 | return 377 | } 378 | 379 | defaultLogger = logger.Clone() 380 | 381 | // adjust already registered scopes 382 | for _, scopes := range uninitialized { 383 | l := defaultLogger.Clone() 384 | for _, sc := range scopes { 385 | if sc.ctx != nil { 386 | l = l.Context(sc.ctx) 387 | } 388 | if sc.metric != nil { 389 | l = l.Metric(sc.metric) 390 | } 391 | if len(sc.kvs) > 0 { 392 | l = l.With(sc.kvs...) 393 | } 394 | l.SetLevel(sc.Level()) 395 | 396 | sc.logger = l 397 | sc.kvs = nil 398 | sc.ctx = nil 399 | sc.metric = nil 400 | sc.level = nil 401 | } 402 | } 403 | uninitialized = nil 404 | } 405 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------