From 6f1fbfeb04dbfd4c62f5b8753819442a965cfb53 Mon Sep 17 00:00:00 2001 From: Phil Kedy Date: Sat, 10 Apr 2021 14:50:34 -0400 Subject: [PATCH] Moved logger from dapr/dapr (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Moved logger from dapr/dapr Co-authored-by: Yaron Schneider Co-authored-by: Artur Souza Co-authored-by: Mukundan Sundararajan Co-authored-by: Young Bu Park Co-authored-by: Sky/敖小剑 Co-authored-by: Joni Collinge Co-authored-by: Jigar Co-authored-by: Ben Wells Co-authored-by: yellow chicks --- .github/ISSUE_TEMPLATE/bug_report.md | 31 ++++ .github/ISSUE_TEMPLATE/discussion.md | 8 + .github/ISSUE_TEMPLATE/feature_request.md | 19 +++ .github/ISSUE_TEMPLATE/proposal.md | 9 + .github/ISSUE_TEMPLATE/question.md | 9 + .github/pull_request_template.md | 16 ++ .github/workflows/kit.yml | 67 ++++++++ CODEOWNERS | 2 + CONTRIBUTING.md | 90 ++++++++++ Makefile | 78 +++++++++ README.md | 5 + go.mod | 9 + go.sum | 19 +++ logger/dapr_logger.go | 144 ++++++++++++++++ logger/dapr_logger_test.go | 198 ++++++++++++++++++++++ logger/logger.go | 132 +++++++++++++++ logger/logger_test.go | 63 +++++++ logger/options.go | 95 +++++++++++ logger/options_test.go | 85 ++++++++++ 19 files changed, 1079 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/discussion.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/proposal.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/kit.yml create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logger/dapr_logger.go create mode 100644 logger/dapr_logger_test.go create mode 100644 logger/logger.go create mode 100644 logger/logger_test.go create mode 100644 logger/options.go create mode 100644 logger/options_test.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9ae3c00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Report a bug in Kit +title: '' +labels: kind/bug +assignees: '' + +--- +## Expected Behavior + + + + +## Actual Behavior + + + + +## Steps to Reproduce the Problem + + + +## Release Note + + + + + + + +RELEASE NOTE: diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md new file mode 100644 index 0000000..7e2b205 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -0,0 +1,8 @@ +--- +name: Feature Request +about: Start a discussion for Kit +title: '' +labels: kind/discussion +assignees: '' + +--- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e9ca44a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Create a Feature Request for Kit +title: '' +labels: kind/enhancement +assignees: '' + +--- +## Describe the feature + +## Release Note + + + + + + + +RELEASE NOTE: diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000..41ce019 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,9 @@ +--- +name: Proposal +about: Create a proposal for Kit +title: '' +labels: kind/proposal +assignees: '' + +--- +## Describe the proposal diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..427342a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,9 @@ +--- +name: Question +about: Ask a question about Kit +title: '' +labels: kind/question +assignees: '' + +--- +## Ask your question here diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7d928cd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,16 @@ +# Description + +_Please explain the changes you've made_ + +## Issue reference + +We strive to have all PR being opened based on an issue, where the problem or feature have been discussed prior to implementation. + +Please reference the issue this PR will close: #_[issue number]_ + +## Checklist + +Please make sure you've completed the relevant tasks for this PR, out of the following list: + +* [ ] Code compiles correctly +* [ ] Created/updated tests diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml new file mode 100644 index 0000000..9132e61 --- /dev/null +++ b/.github/workflows/kit.yml @@ -0,0 +1,67 @@ +# ------------------------------------------------------------ +# Copyright (c) Microsoft Corporation and Dapr Contributors. +# Licensed under the MIT License. +# ------------------------------------------------------------ + +name: kit + +on: + push: + branches: + - main + - release-* + tags: + - v* + pull_request: + branches: + - main + - release-* +jobs: + build: + name: Build ${{ matrix.target_os }}_${{ matrix.target_arch }} binaries + runs-on: ${{ matrix.os }} + env: + GOVER: 1.16 + GOOS: ${{ matrix.target_os }} + GOARCH: ${{ matrix.target_arch }} + GOPROXY: https://proxy.golang.org + GOLANGCI_LINT_VER: v1.31 + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + target_arch: [arm, amd64] + include: + - os: ubuntu-latest + target_os: linux + - os: windows-latest + target_os: windows + - os: macOS-latest + target_os: darwin + exclude: + - os: windows-latest + target_arch: arm + - os: macOS-latest + target_arch: arm + steps: + - name: Set up Go ${{ env.GOVER }} + uses: actions/setup-go@v1 + with: + go-version: ${{ env.GOVER }} + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Run golangci-lint + if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' + uses: golangci/golangci-lint-action@v2.2.1 + with: + version: ${{ env.GOLANGCI_LINT_VER }} + - name: Run make go.mod check-diff + if: matrix.target_arch != 'arm' + run: make go.mod check-diff + - name: Run make test + env: + COVERAGE_OPTS: "-coverprofile=coverage.txt -covermode=atomic" + if: matrix.target_arch != 'arm' + run: make test + - name: Codecov + if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' + uses: codecov/codecov-action@v1 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..61b8eef --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# These owners are the maintainers and approvers of this repo +* @maintainers-kit @approvers-kit \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a412d17 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contribution Guidelines + +Thank you for your interest in Dapr! + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. + +For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +This project has adopted the Microsoft Open Source Code of Conduct. +For more information see the Code of Conduct FAQ +or contact opencode@microsoft.com with any additional questions or comments. + +Contributions come in many forms: submitting issues, writing code, participating in discussions and community calls. + +This document provides the guidelines for how to contribute to the Dapr project. + +## Issues + +This section describes the guidelines for submitting issues + +### Issue Types + +There are 4 types of issues: + +- Issue/Bug: You've found a bug with the code, and want to report it, or create an issue to track the bug. +- Issue/Discussion: You have something on your mind, which requires input form others in a discussion, before it eventually manifests as a proposal. +- Issue/Proposal: Used for items that propose a new idea or functionality. This allows feedback from others before code is written. +- Issue/Question: Use this issue type, if you need help or have a question. + +### Before You File + +Before you file an issue, make sure you've checked the following: + +1. Is it the right repository? + - The Dapr project is distributed across multiple repositories. Check the list of [repositories](/~https://github.com/dapr) if you aren't sure which repo is the correct one. +1. Check for existing issues + - Before you create a new issue, please do a search in [open issues](/~https://github.com/dapr/components-contrib/issues) to see if the issue or feature request has already been filed. + - If you find your issue already exists, make relevant comments and add your [reaction](/~https://github.com/blog/2119-add-reaction-to-pull-requests-issues-and-comments). Use a reaction: + - 👍 up-vote + - 👎 down-vote +1. For bugs + - Check it's not an environment issue. For example, if running on Kubernetes, make sure prerequisites are in place. (state stores, bindings, etc.) + - You have as much data as possible. This usually comes in the form of logs and/or stacktrace. If running on Kubernetes or other environment, look at the logs of the Dapr services (runtime, operator, placement service). More details on how to get logs can be found [here](https://docs.dapr.io/operations/troubleshooting/logs-troubleshooting/). +1. For proposals + - Many changes to the Dapr runtime may require changes to the API. In that case, the best place to discuss the potential feature is the main [Dapr repo](/~https://github.com/dapr/dapr). + - Other examples could include bindings, state stores or entirely new components. + +## Contributing to Dapr + +This section describes the guidelines for contributing code / docs to Dapr. + +### Pull Requests + +All contributions come through pull requests. To submit a proposed change, we recommend following this workflow: + +1. Make sure there's an issue (bug or proposal) raised, which sets the expectations for the contribution you are about to make. +1. Fork the relevant repo and create a new branch +1. Create your change + - Code changes require tests +1. Update relevant documentation for the change +1. Commit and open a PR +1. Wait for the CI process to finish and make sure all checks are green +1. A maintainer of the project will be assigned, and you can expect a review within a few days + +#### Use work-in-progress PRs for early feedback + +A good way to communicate before investing too much time is to create a "Work-in-progress" PR and share it with your reviewers. The standard way of doing this is to add a "[WIP]" prefix in your PR's title and assign the **do-not-merge** label. This will let people looking at your PR know that it is not well baked yet. + +### Use of Third-party code + +- All third-party code must be placed in the `vendor/` folder. +- `vendor/` folder is managed by Go modules and stores the source code of third-party Go dependencies. - The `vendor/` folder should not be modified manually. +- Third-party code must include licenses. + +A non-exclusive list of code that must be places in `vendor/`: + +- Open source, free software, or commercially-licensed code. +- Tools or libraries or protocols that are open source, free software, or commercially licensed. + +**Thank You!** - Your contributions to open source, large or small, make projects like this possible. Thank you for taking the time to contribute. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24d5fbc --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +# ------------------------------------------------------------ +# Copyright (c) Microsoft Corporation and Dapr Contributors. +# Licensed under the MIT License. +# ------------------------------------------------------------ + +################################################################################ +# Variables # +################################################################################ + +export GO111MODULE ?= on +export GOPROXY ?= https://proxy.golang.org +export GOSUMDB ?= sum.golang.org + +GIT_COMMIT = $(shell git rev-list -1 HEAD) +GIT_VERSION = $(shell git describe --always --abbrev=7 --dirty) +# By default, disable CGO_ENABLED. See the details on https://golang.org/cmd/cgo +CGO ?= 0 + +LOCAL_ARCH := $(shell uname -m) +ifeq ($(LOCAL_ARCH),x86_64) + TARGET_ARCH_LOCAL=amd64 +else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8) + TARGET_ARCH_LOCAL=arm64 +else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv) + TARGET_ARCH_LOCAL=arm +else + TARGET_ARCH_LOCAL=amd64 +endif +export GOARCH ?= $(TARGET_ARCH_LOCAL) + +LOCAL_OS := $(shell uname) +ifeq ($(LOCAL_OS),Linux) + TARGET_OS_LOCAL = linux +else ifeq ($(LOCAL_OS),Darwin) + TARGET_OS_LOCAL = darwin +else + TARGET_OS_LOCAL ?= windows +endif +export GOOS ?= $(TARGET_OS_LOCAL) + +ifeq ($(GOOS),windows) +BINARY_EXT_LOCAL:=.exe +GOLANGCI_LINT:=golangci-lint.exe +# Workaround for /~https://github.com/golang/go/issues/40795 +BUILDMODE:=-buildmode=exe +else +BINARY_EXT_LOCAL:= +GOLANGCI_LINT:=golangci-lint +endif + +################################################################################ +# Target: test # +################################################################################ +.PHONY: test +test: + go test ./... $(COVERAGE_OPTS) $(BUILDMODE) + +################################################################################ +# Target: lint # +################################################################################ +.PHONY: lint +lint: + # Due to /~https://github.com/golangci/golangci-lint/issues/580, we need to add --fix for windows + $(GOLANGCI_LINT) run --timeout=20m + +################################################################################ +# Target: go.mod # +################################################################################ +.PHONY: go.mod +go.mod: + go mod tidy + +################################################################################ +# Target: check-diff # +################################################################################ +.PHONY: check-diff +check-diff: + git diff --exit-code ./go.mod # check no changes diff --git a/README.md b/README.md index 60b7f63..4414c05 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # kit + Shared utility code for Dapr runtime + +## Code of Conduct + +Please refer to our [Dapr Community Code of Conduct](/~https://github.com/dapr/community/blob/master/CODE-OF-CONDUCT.md) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..10952a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/dapr/kit + +go 1.16 + +require ( + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/testify v1.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9bcb37b --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logger/dapr_logger.go b/logger/dapr_logger.go new file mode 100644 index 0000000..879e951 --- /dev/null +++ b/logger/dapr_logger.go @@ -0,0 +1,144 @@ +package logger + +import ( + "os" + "time" + + "github.com/sirupsen/logrus" +) + +// daprLogger is the implemention for logrus +type daprLogger struct { + // name is the name of logger that is published to log as a scope + name string + // loger is the instance of logrus logger + logger *logrus.Entry +} + +var DaprVersion string = "unknown" + +func newDaprLogger(name string) *daprLogger { + newLogger := logrus.New() + newLogger.SetOutput(os.Stdout) + + dl := &daprLogger{ + name: name, + logger: newLogger.WithFields(logrus.Fields{ + logFieldScope: name, + logFieldType: LogTypeLog, + }), + } + + dl.EnableJSONOutput(defaultJSONOutput) + + return dl +} + +// EnableJSONOutput enables JSON formatted output log +func (l *daprLogger) EnableJSONOutput(enabled bool) { + var formatter logrus.Formatter + + fieldMap := logrus.FieldMap{ + // If time field name is conflicted, logrus adds "fields." prefix. + // So rename to unused field @time to avoid the confliction. + logrus.FieldKeyTime: logFieldTimeStamp, + logrus.FieldKeyLevel: logFieldLevel, + logrus.FieldKeyMsg: logFieldMessage, + } + + hostname, _ := os.Hostname() + l.logger.Data = logrus.Fields{ + logFieldScope: l.logger.Data[logFieldScope], + logFieldType: LogTypeLog, + logFieldInstance: hostname, + logFieldDaprVer: DaprVersion, + } + + if enabled { + formatter = &logrus.JSONFormatter{ + TimestampFormat: time.RFC3339Nano, + FieldMap: fieldMap, + } + } else { + formatter = &logrus.TextFormatter{ + TimestampFormat: time.RFC3339Nano, + FieldMap: fieldMap, + } + } + + l.logger.Logger.SetFormatter(formatter) +} + +// SetAppID sets app_id field in the log. Default value is empty string +func (l *daprLogger) SetAppID(id string) { + l.logger = l.logger.WithField(logFieldAppID, id) +} + +func toLogrusLevel(lvl LogLevel) logrus.Level { + // ignore error because it will never happens + l, _ := logrus.ParseLevel(string(lvl)) + return l +} + +// SetOutputLevel sets log output level +func (l *daprLogger) SetOutputLevel(outputLevel LogLevel) { + l.logger.Logger.SetLevel(toLogrusLevel(outputLevel)) +} + +// WithLogType specify the log_type field in log. Default value is LogTypeLog +func (l *daprLogger) WithLogType(logType string) Logger { + return &daprLogger{ + name: l.name, + logger: l.logger.WithField(logFieldType, logType), + } +} + +// Info logs a message at level Info. +func (l *daprLogger) Info(args ...interface{}) { + l.logger.Log(logrus.InfoLevel, args...) +} + +// Infof logs a message at level Info. +func (l *daprLogger) Infof(format string, args ...interface{}) { + l.logger.Logf(logrus.InfoLevel, format, args...) +} + +// Debug logs a message at level Debug. +func (l *daprLogger) Debug(args ...interface{}) { + l.logger.Log(logrus.DebugLevel, args...) +} + +// Debugf logs a message at level Debug. +func (l *daprLogger) Debugf(format string, args ...interface{}) { + l.logger.Logf(logrus.DebugLevel, format, args...) +} + +// Warn logs a message at level Warn. +func (l *daprLogger) Warn(args ...interface{}) { + l.logger.Log(logrus.WarnLevel, args...) +} + +// Warnf logs a message at level Warn. +func (l *daprLogger) Warnf(format string, args ...interface{}) { + l.logger.Logf(logrus.WarnLevel, format, args...) +} + +// Error logs a message at level Error. +func (l *daprLogger) Error(args ...interface{}) { + l.logger.Log(logrus.ErrorLevel, args...) +} + +// Errorf logs a message at level Error. +func (l *daprLogger) Errorf(format string, args ...interface{}) { + l.logger.Logf(logrus.ErrorLevel, format, args...) +} + +// Fatal logs a message at level Fatal then the process will exit with status set to 1. +func (l *daprLogger) Fatal(args ...interface{}) { + l.logger.Fatal(args...) +} + +// Fatalf logs a message at level Fatal then the process will exit with status set to 1. +func (l *daprLogger) Fatalf(format string, args ...interface{}) { + l.logger.Fatalf(format, args...) +} diff --git a/logger/dapr_logger_test.go b/logger/dapr_logger_test.go new file mode 100644 index 0000000..90ef998 --- /dev/null +++ b/logger/dapr_logger_test.go @@ -0,0 +1,198 @@ +package logger + +import ( + "bytes" + "encoding/json" + "io" + "os" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +const fakeLoggerName = "fakeLogger" + +func getTestLogger(buf io.Writer) *daprLogger { + l := newDaprLogger(fakeLoggerName) + l.logger.Logger.SetOutput(buf) + + return l +} + +func TestEnableJSON(t *testing.T) { + var buf bytes.Buffer + testLogger := getTestLogger(&buf) + + expectedHost, _ := os.Hostname() + testLogger.EnableJSONOutput(true) + _, okJSON := testLogger.logger.Logger.Formatter.(*logrus.JSONFormatter) + assert.True(t, okJSON) + assert.Equal(t, "fakeLogger", testLogger.logger.Data[logFieldScope]) + assert.Equal(t, LogTypeLog, testLogger.logger.Data[logFieldType]) + assert.Equal(t, expectedHost, testLogger.logger.Data[logFieldInstance]) + + testLogger.EnableJSONOutput(false) + _, okText := testLogger.logger.Logger.Formatter.(*logrus.TextFormatter) + assert.True(t, okText) + assert.Equal(t, "fakeLogger", testLogger.logger.Data[logFieldScope]) + assert.Equal(t, LogTypeLog, testLogger.logger.Data[logFieldType]) + assert.Equal(t, expectedHost, testLogger.logger.Data[logFieldInstance]) +} + +func TestJSONLoggerFields(t *testing.T) { + tests := []struct { + name string + outputLevel LogLevel + level string + appID string + message string + instance string + fn func(*daprLogger, string) + }{ + { + "info()", + InfoLevel, + "info", + "dapr_app", + "King Dapr", + "dapr-pod", + func(l *daprLogger, msg string) { + l.Info(msg) + }, + }, + { + "infof()", + InfoLevel, + "info", + "dapr_app", + "King Dapr", + "dapr-pod", + func(l *daprLogger, msg string) { + l.Infof("%s", msg) + }, + }, + { + "debug()", + DebugLevel, + "debug", + "dapr_app", + "King Dapr", + "dapr-pod", + func(l *daprLogger, msg string) { + l.Debug(msg) + }, + }, + { + "debugf()", + DebugLevel, + "debug", + "dapr_app", + "King Dapr", + "dapr-pod", + func(l *daprLogger, msg string) { + l.Debugf("%s", msg) + }, + }, + { + "error()", + InfoLevel, + "error", + "dapr_app", + "King Dapr", + "dapr-pod", + func(l *daprLogger, msg string) { + l.Error(msg) + }, + }, + { + "errorf()", + InfoLevel, + "error", + "dapr_app", + "King Dapr", + "dapr-pod", + func(l *daprLogger, msg string) { + l.Errorf("%s", msg) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + testLogger := getTestLogger(&buf) + testLogger.EnableJSONOutput(true) + testLogger.SetAppID(tt.appID) + DaprVersion = tt.appID + testLogger.SetOutputLevel(tt.outputLevel) + testLogger.logger.Data[logFieldInstance] = tt.instance + + tt.fn(testLogger, tt.message) + + b, _ := buf.ReadBytes('\n') + var o map[string]interface{} + assert.NoError(t, json.Unmarshal(b, &o)) + + // assert + assert.Equal(t, tt.appID, o[logFieldAppID]) + assert.Equal(t, tt.instance, o[logFieldInstance]) + assert.Equal(t, tt.level, o[logFieldLevel]) + assert.Equal(t, LogTypeLog, o[logFieldType]) + assert.Equal(t, fakeLoggerName, o[logFieldScope]) + assert.Equal(t, tt.message, o[logFieldMessage]) + _, err := time.Parse(time.RFC3339, o[logFieldTimeStamp].(string)) + assert.NoError(t, err) + }) + } +} + +func TestWithTypeFields(t *testing.T) { + var buf bytes.Buffer + testLogger := getTestLogger(&buf) + testLogger.EnableJSONOutput(true) + testLogger.SetAppID("dapr_app") + testLogger.SetOutputLevel(InfoLevel) + + // WithLogType will return new Logger with request log type + // Meanwhile, testLogger uses the default logtype + loggerWithRequestType := testLogger.WithLogType(LogTypeRequest) + loggerWithRequestType.Info("call user app") + + b, _ := buf.ReadBytes('\n') + var o map[string]interface{} + assert.NoError(t, json.Unmarshal(b, &o)) + + assert.Equalf(t, LogTypeRequest, o[logFieldType], "new logger must be %s type", LogTypeRequest) + + // Log our via testLogger to ensure that testLogger still uses the default logtype + testLogger.Info("testLogger with log LogType") + + b, _ = buf.ReadBytes('\n') + assert.NoError(t, json.Unmarshal(b, &o)) + + assert.Equalf(t, LogTypeLog, o[logFieldType], "testLogger must be %s type", LogTypeLog) +} + +func TestToLogrusLevel(t *testing.T) { + t.Run("Dapr DebugLevel to Logrus.DebugLevel", func(t *testing.T) { + assert.Equal(t, logrus.DebugLevel, toLogrusLevel(DebugLevel)) + }) + + t.Run("Dapr InfoLevel to Logrus.InfoLevel", func(t *testing.T) { + assert.Equal(t, logrus.InfoLevel, toLogrusLevel(InfoLevel)) + }) + + t.Run("Dapr WarnLevel to Logrus.WarnLevel", func(t *testing.T) { + assert.Equal(t, logrus.WarnLevel, toLogrusLevel(WarnLevel)) + }) + + t.Run("Dapr ErrorLevel to Logrus.ErrorLevel", func(t *testing.T) { + assert.Equal(t, logrus.ErrorLevel, toLogrusLevel(ErrorLevel)) + }) + + t.Run("Dapr FatalLevel to Logrus.FatalLevel", func(t *testing.T) { + assert.Equal(t, logrus.FatalLevel, toLogrusLevel(FatalLevel)) + }) +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..931fc1f --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,132 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package logger + +import ( + "strings" + "sync" +) + +const ( + // LogTypeLog is normal log type + LogTypeLog = "log" + // LogTypeRequest is Request log type + LogTypeRequest = "request" + + // Field names that defines Dapr log schema + logFieldTimeStamp = "time" + logFieldLevel = "level" + logFieldType = "type" + logFieldScope = "scope" + logFieldMessage = "msg" + logFieldInstance = "instance" + logFieldDaprVer = "ver" + logFieldAppID = "app_id" +) + +// LogLevel is Dapr Logger Level type +type LogLevel string + +const ( + // DebugLevel has verbose message + DebugLevel LogLevel = "debug" + // InfoLevel is default log level + InfoLevel LogLevel = "info" + // WarnLevel is for logging messages about possible issues + WarnLevel LogLevel = "warn" + // ErrorLevel is for logging errors + ErrorLevel LogLevel = "error" + // FatalLevel is for logging fatal messages. The system shuts down after logging the message. + FatalLevel LogLevel = "fatal" + + // UndefinedLevel is for undefined log level + UndefinedLevel LogLevel = "undefined" +) + +// globalLoggers is the collection of Dapr Logger that is shared globally. +// TODO: User will disable or enable logger on demand. +var globalLoggers = map[string]Logger{} +var globalLoggersLock = sync.RWMutex{} + +// Logger includes the logging api sets +type Logger interface { + // EnableJSONOutput enables JSON formatted output log + EnableJSONOutput(enabled bool) + + // SetAppID sets dapr_id field in the log. Default value is empty string + SetAppID(id string) + // SetOutputLevel sets log output level + SetOutputLevel(outputLevel LogLevel) + + // WithLogType specify the log_type field in log. Default value is LogTypeLog + WithLogType(logType string) Logger + + // Info logs a message at level Info. + Info(args ...interface{}) + // Infof logs a message at level Info. + Infof(format string, args ...interface{}) + // Debug logs a message at level Debug. + Debug(args ...interface{}) + // Debugf logs a message at level Debug. + Debugf(format string, args ...interface{}) + // Warn logs a message at level Warn. + Warn(args ...interface{}) + // Warnf logs a message at level Warn. + Warnf(format string, args ...interface{}) + // Error logs a message at level Error. + Error(args ...interface{}) + // Errorf logs a message at level Error. + Errorf(format string, args ...interface{}) + // Fatal logs a message at level Fatal then the process will exit with status set to 1. + Fatal(args ...interface{}) + // Fatalf logs a message at level Fatal then the process will exit with status set to 1. + Fatalf(format string, args ...interface{}) +} + +// toLogLevel converts to LogLevel +func toLogLevel(level string) LogLevel { + switch strings.ToLower(level) { + case "debug": + return DebugLevel + case "info": + return InfoLevel + case "warn": + return WarnLevel + case "error": + return ErrorLevel + case "fatal": + return FatalLevel + } + + // unsupported log level by Dapr + return UndefinedLevel +} + +// NewLogger creates new Logger instance. +func NewLogger(name string) Logger { + globalLoggersLock.Lock() + defer globalLoggersLock.Unlock() + + logger, ok := globalLoggers[name] + if !ok { + logger = newDaprLogger(name) + globalLoggers[name] = logger + } + + return logger +} + +func getLoggers() map[string]Logger { + globalLoggersLock.RLock() + defer globalLoggersLock.RUnlock() + + l := map[string]Logger{} + for k, v := range globalLoggers { + l[k] = v + } + + return l +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..386a68e --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,63 @@ +package logger + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func clearLoggers() { + globalLoggers = map[string]Logger{} +} + +func TestNewLogger(t *testing.T) { + testLoggerName := "dapr.test" + + t.Run("create new logger instance", func(t *testing.T) { + clearLoggers() + + // act + NewLogger(testLoggerName) + _, ok := globalLoggers[testLoggerName] + + // assert + assert.True(t, ok) + }) + + t.Run("return the existing logger instance", func(t *testing.T) { + clearLoggers() + + // act + oldLogger := NewLogger(testLoggerName) + newLogger := NewLogger(testLoggerName) + + // assert + assert.Equal(t, oldLogger, newLogger) + }) +} + +func TestToLogLevel(t *testing.T) { + t.Run("convert debug to DebugLevel", func(t *testing.T) { + assert.Equal(t, DebugLevel, toLogLevel("debug")) + }) + + t.Run("convert info to InfoLevel", func(t *testing.T) { + assert.Equal(t, InfoLevel, toLogLevel("info")) + }) + + t.Run("convert warn to WarnLevel", func(t *testing.T) { + assert.Equal(t, WarnLevel, toLogLevel("warn")) + }) + + t.Run("convert error to ErrorLevel", func(t *testing.T) { + assert.Equal(t, ErrorLevel, toLogLevel("error")) + }) + + t.Run("convert fatal to FatalLevel", func(t *testing.T) { + assert.Equal(t, FatalLevel, toLogLevel("fatal")) + }) + + t.Run("undefined loglevel", func(t *testing.T) { + assert.Equal(t, UndefinedLevel, toLogLevel("undefined")) + }) +} diff --git a/logger/options.go b/logger/options.go new file mode 100644 index 0000000..86f045d --- /dev/null +++ b/logger/options.go @@ -0,0 +1,95 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package logger + +import ( + "github.com/pkg/errors" +) + +const ( + defaultJSONOutput = false + defaultOutputLevel = "info" + undefinedAppID = "" +) + +// Options defines the sets of options for Dapr logging +type Options struct { + // appID is the unique id of Dapr Application + appID string + + // JSONFormatEnabled is the flag to enable JSON formatted log + JSONFormatEnabled bool + + // OutputLevel is the level of logging + OutputLevel string +} + +// SetOutputLevel sets the log output level +func (o *Options) SetOutputLevel(outputLevel string) error { + if toLogLevel(outputLevel) == UndefinedLevel { + return errors.Errorf("undefined Log Output Level: %s", outputLevel) + } + o.OutputLevel = outputLevel + return nil +} + +// SetAppID sets Application ID +func (o *Options) SetAppID(id string) { + o.appID = id +} + +// AttachCmdFlags attaches log options to command flags +func (o *Options) AttachCmdFlags( + stringVar func(p *string, name string, value string, usage string), + boolVar func(p *bool, name string, value bool, usage string)) { + if stringVar != nil { + stringVar( + &o.OutputLevel, + "log-level", + defaultOutputLevel, + "Options are debug, info, warn, error, or fatal (default info)") + } + if boolVar != nil { + boolVar( + &o.JSONFormatEnabled, + "log-as-json", + defaultJSONOutput, + "print log as JSON (default false)") + } +} + +// DefaultOptions returns default values of Options +func DefaultOptions() Options { + return Options{ + JSONFormatEnabled: defaultJSONOutput, + appID: undefinedAppID, + OutputLevel: defaultOutputLevel, + } +} + +// ApplyOptionsToLoggers applys options to all registered loggers +func ApplyOptionsToLoggers(options *Options) error { + internalLoggers := getLoggers() + + // Apply formatting options first + for _, v := range internalLoggers { + v.EnableJSONOutput(options.JSONFormatEnabled) + + if options.appID != undefinedAppID { + v.SetAppID(options.appID) + } + } + + daprLogLevel := toLogLevel(options.OutputLevel) + if daprLogLevel == UndefinedLevel { + return errors.Errorf("invalid value for --log-level: %s", options.OutputLevel) + } + + for _, v := range internalLoggers { + v.SetOutputLevel(daprLogLevel) + } + return nil +} diff --git a/logger/options_test.go b/logger/options_test.go new file mode 100644 index 0000000..7b9ab8d --- /dev/null +++ b/logger/options_test.go @@ -0,0 +1,85 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package logger + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptions(t *testing.T) { + t.Run("default options", func(t *testing.T) { + o := DefaultOptions() + assert.Equal(t, defaultJSONOutput, o.JSONFormatEnabled) + assert.Equal(t, undefinedAppID, o.appID) + assert.Equal(t, defaultOutputLevel, o.OutputLevel) + }) + + t.Run("set dapr ID", func(t *testing.T) { + o := DefaultOptions() + assert.Equal(t, undefinedAppID, o.appID) + + o.SetAppID("dapr-app") + assert.Equal(t, "dapr-app", o.appID) + }) + + t.Run("attaching log related cmd flags", func(t *testing.T) { + o := DefaultOptions() + + logLevelAsserted := false + testStringVarFn := func(p *string, name string, value string, usage string) { + if name == "log-level" && value == defaultOutputLevel { + logLevelAsserted = true + } + } + + logAsJSONAsserted := false + testBoolVarFn := func(p *bool, name string, value bool, usage string) { + if name == "log-as-json" && value == defaultJSONOutput { + logAsJSONAsserted = true + } + } + + o.AttachCmdFlags(testStringVarFn, testBoolVarFn) + + // assert + assert.True(t, logLevelAsserted) + assert.True(t, logAsJSONAsserted) + }) +} + +func TestApplyOptionsToLoggers(t *testing.T) { + testOptions := Options{ + JSONFormatEnabled: true, + appID: "dapr-app", + OutputLevel: "debug", + } + + // Create two loggers + testLoggers := []Logger{ + NewLogger("testLogger0"), + NewLogger("testLogger1"), + } + + for _, l := range testLoggers { + l.EnableJSONOutput(false) + l.SetOutputLevel(InfoLevel) + } + + assert.NoError(t, ApplyOptionsToLoggers(&testOptions)) + + for _, l := range testLoggers { + assert.Equal( + t, + "dapr-app", + (l.(*daprLogger)).logger.Data[logFieldAppID]) + assert.Equal( + t, + toLogrusLevel(DebugLevel), + (l.(*daprLogger)).logger.Logger.GetLevel()) + } +}