diff --git a/.github/ISSUE_TEMPLATE/bug_report--ldotel.md b/.github/ISSUE_TEMPLATE/bug_report--ldotel.md new file mode 100644 index 00000000..bd51435e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report--ldotel.md @@ -0,0 +1,37 @@ +--- +name: Bug report for the ldotel module +about: Create a report to help us improve +title: '' +labels: 'ldotel, enhancement' +assignees: '' + +--- + +**Is this a support request?** +This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com. + +Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add any log output related to your problem. + +**SDK version** +The version of this SDK that you are using. + +**Language version, developer tools** +For instance, Go 1.22. + +**OS/platform** +For instance, Ubuntu 16.04, or Windows 10. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a9c8db85..6ad037e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: '' +labels: 'server-sdk, bug' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request--ldotel.md b/.github/ISSUE_TEMPLATE/feature_request--ldotel.md new file mode 100644 index 00000000..435960c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request--ldotel.md @@ -0,0 +1,20 @@ +--- +name: Feature request for the ldotel module +about: Suggest an idea for this project +title: '' +labels: 'ldotel, enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 3f7d5bf3..4c3878f6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: '' -labels: '' +labels: 'server-sdk, enhancement' assignees: '' --- diff --git a/.github/actions/unit-tests/action.yml b/.github/actions/unit-tests/action.yml index 623a1313..41eb1e8e 100644 --- a/.github/actions/unit-tests/action.yml +++ b/.github/actions/unit-tests/action.yml @@ -5,6 +5,9 @@ inputs: description: 'Whether to run linters.' required: false default: 'false' + test-target: + description: 'The test target to run.' + required: true runs: using: composite @@ -19,7 +22,7 @@ runs: - name: Test shell: bash id: test - run: make test | tee raw_report.txt + run: make ${{ inputs.test-target }} | tee raw_report.txt - name: Process test results if: steps.test.outcome == 'success' @@ -31,5 +34,5 @@ runs: if: steps.process-test.outcome == 'success' uses: actions/upload-artifact@v4 with: - name: Test-result-${{ steps.go-version.outputs.version }} + name: Test-result-${{ inputs.test-target }}${{ steps.go-version.outputs.version }} path: junit_report.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49885396..d958bccd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Build and Test +name: Build and Test SDK on: push: branches: [ 'v7', 'feat/**' ] diff --git a/.github/workflows/common_ci.yml b/.github/workflows/common_ci.yml index 0c959f3f..f0e4e4bb 100644 --- a/.github/workflows/common_ci.yml +++ b/.github/workflows/common_ci.yml @@ -7,7 +7,6 @@ on: required: true type: string - jobs: unit-test-and-coverage: runs-on: ubuntu-latest @@ -21,6 +20,7 @@ jobs: - uses: ./.github/actions/unit-tests with: lint: 'true' + test-target: sdk-test - uses: ./.github/actions/coverage with: enforce: 'false' @@ -51,8 +51,6 @@ jobs: name: Contract-test-service-logs-${{ steps.go-version.outputs.version }} path: /tmp/sdk-contract-test-service.log - - benchmarks: name: 'Benchmarks' runs-on: ubuntu-latest diff --git a/.github/workflows/ldotel-ci.yml b/.github/workflows/ldotel-ci.yml new file mode 100644 index 00000000..98706523 --- /dev/null +++ b/.github/workflows/ldotel-ci.yml @@ -0,0 +1,38 @@ +name: Build and Test ldotel +on: + push: + branches: [ 'v7', 'feat/**' ] + paths-ignore: + - '**.md' # Don't run CI on markdown changes. + pull_request: + branches: [ 'v7', 'feat/**' ] + paths-ignore: + - '**.md' + +jobs: + go-versions: + uses: ./.github/workflows/go-versions.yml + + # Runs the common tasks (unit tests, lint, contract tests) for each Go version. + test-linux: + name: ${{ format('ldotel Linux, Go {0}', matrix.go-version) }} + needs: go-versions + strategy: + # Let jobs fail independently, in case it's a single version that's broken. + fail-fast: false + matrix: + go-version: ${{ fromJSON(needs.go-versions.outputs.matrix) }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Go ${{ inputs.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - uses: ./.github/actions/unit-tests + with: + lint: 'true' + test-target: ldotel-test + - uses: ./.github/actions/coverage + with: + enforce: 'false' diff --git a/.gitignore b/.gitignore index 95ea7aed..d23ddea7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ go-server-sdk.test allocations.out .idea .vscode +go.work +go.work.sum diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cfc6866..37c3ccd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,28 +18,39 @@ This project should be built against the lowest supported Go version as describe ### Building -To build the SDK without running any tests: -``` +To build all modules without running any tests: +```shell make ``` -If you wish to clean your working directory between builds, you can clean it by running: -``` -make clean -``` - To run the linter: -``` +```shell make lint ``` ### Testing -To build the SDK and run all unit tests: -``` +To build all modules and run all unit tests: +```shell make test ``` +### Clean + +To clean temporary files created by other targets: +```shell +make clean +``` + +### Working Cross Module + +If you have a change which affects more than a single module, then you can use a go workspace. + +You can create a workspace using: +```shell +make workspace +``` + ## Coding best practices The Go SDK can be used in high-traffic application/service code where performance is critical. There are a number of coding principles to keep in mind for maximizing performance. The benchmarks that are run in CI are helpful in measuring the impact of code changes in this regard. diff --git a/Makefile b/Makefile index c53b0b53..f9a52d31 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ GOLANGCI_LINT_VERSION=v1.57.1 LINTER=./bin/golangci-lint LINTER_VERSION_FILE=./bin/.golangci-lint-version-$(GOLANGCI_LINT_VERSION) +GO_WORK_FILE=go.work +GO_WORK_SUM=go.work.sum + TEST_BINARY=./go-server-sdk.test ALLOCATIONS_LOG=./build/allocations.out @@ -20,22 +23,59 @@ COVERAGE_ENFORCER_FLAGS=-package github.com/launchdarkly/go-server-sdk/v7 \ -skipcode "// COVERAGE" \ -packagestats -filestats -showcode -.PHONY: build clean test test-coverage benchmarks benchmark-allocs lint +ALL_BUILD_TARGETS=sdk ldotel +ALL_TEST_TARGETS = $(addsuffix -test, $(ALL_BUILD_TARGETS)) +ALL_LINT_TARGETS = $(addsuffix -lint, $(ALL_BUILD_TARGETS)) -build: - go build ./... +.PHONY: all build clean test test-coverage benchmarks benchmark-allocs lint workspace workspace-clean $(ALL_BUILD_TARGETS) $(ALL_TEST_TARGETS) $(ALL_LINT_TARGETS) + +all: $(ALL_BUILD_TARGETS) + +test: $(ALL_TEST_TARGETS) -clean: - go clean +clean: workspace-clean + rm -rf ./bin/ -test: - go test -run=not-a-real-test ./... # just ensures that the tests compile +sdk: + go build ./... + +sdk-test: go test -v -race ./... @# The proxy tests must be run separately because Go caches the global proxy environment variables. We use @# build tags to isolate these tests from the main test run so that if you do "go test ./..." you won't @# get unexpected errors. for tag in proxytest1 proxytest2; do go test -race -v -tags=$$tag ./proxytest; done +sdk-lint: + $(LINTER) run ./... + +ldotel: + @if [ -f go.work ]; then \ + echo "Building ldotel with workspace" \ + go build ./ldotel; \ + else \ + echo "Building ldotel without workspace" \ + cd ldotel && go build .; \ + fi + +ldotel-test: + @if [ -f go.work ]; then \ + echo "Testing ldotel with workspace" \ + go test -v -race ./ldotel; \ + else \ + echo "Testing ldotel without workspace" \ + cd ldotel && go test -v -race .; \ + fi + +ldotel-lint: + @if [ -f go.work ]; then \ + echo "Linting ldotel with workspace" \ + $(LINTER) run ./ldotel; \ + else \ + echo "Linting ldotel without workspace" \ + cd ldotel && $(LINTER) run .; \ + fi + test-coverage: $(COVERAGE_PROFILE_RAW) go run github.com/launchdarkly-labs/go-coverage-enforcer@latest $(COVERAGE_ENFORCER_FLAGS) -outprofile $(COVERAGE_PROFILE_FILTERED) $(COVERAGE_PROFILE_RAW) go tool cover -html $(COVERAGE_PROFILE_FILTERED) -o $(COVERAGE_PROFILE_FILTERED_HTML) @@ -70,17 +110,25 @@ $(LINTER_VERSION_FILE): curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION) touch $(LINTER_VERSION_FILE) -lint: $(LINTER_VERSION_FILE) - $(LINTER) run ./... - +lint: $(LINTER_VERSION_FILE) $(ALL_LINT_TARGETS) TEMP_TEST_OUTPUT=/tmp/sdk-contract-test-service.log # TEST_HARNESS_PARAMS can be set to add -skip parameters for any contract tests that cannot yet pass TEST_HARNESS_PARAMS= +workspace: go.work + +go.work: + go work init ./ + go work use ./ldotel + go work use ./testservice + +workspace-clean: + rm -f $(GO_WORK_FILE) $(GO_WORK_SUM) + build-contract-tests: - @cd testservice && go mod tidy && go build + @go build -o ./testservice/testservice ./testservice start-contract-test-service: build-contract-tests @./testservice/testservice diff --git a/ldotel/README.md b/ldotel/README.md new file mode 100644 index 00000000..eeb02f66 --- /dev/null +++ b/ldotel/README.md @@ -0,0 +1,54 @@ +LaunchDarkly Server-side OTEL library for Go +============================================== +[![Actions Status](https://github.com/launchdarkly/go-server-sdk/actions/workflows/ldotel-ci.yml/badge.svg?branch=v7)](https://github.com/launchdarkly/go-server-sdk/actions/workflows/ci.yml) + +LaunchDarkly overview +------------------------- +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Getting started +----------- + +Import the module: + +```go +import ( + "github.com/launchdarkly/go-server-sdk/ldotel" +) +``` + +Configure the LaunchDarkly client to use a tracing hook: + +```go +client, _ = ld.MakeCustomClient("your-sdk-key", +ld.Config{ + Hooks: []ldhooks.Hook{ldotel.NewTracingHook()}, +}, 5*time.Second) +``` + +Learn more +----------- + +Read our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. + +Contributing +------------ + +We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this library. + +About LaunchDarkly +----------- + +* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: + * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. + * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates diff --git a/ldotel/go.mod b/ldotel/go.mod new file mode 100644 index 00000000..b5ad4ebb --- /dev/null +++ b/ldotel/go.mod @@ -0,0 +1,35 @@ +module github.com/launchdarkly/go-server-sdk/ldotel + +go 1.21 + +require ( + github.com/launchdarkly/go-sdk-common/v3 v3.1.0 + github.com/launchdarkly/go-server-sdk/v7 v7.3.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/sdk v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/launchdarkly/ccache v1.1.0 // indirect + github.com/launchdarkly/eventsource v1.6.2 // indirect + github.com/launchdarkly/go-jsonstream/v3 v3.0.0 // indirect + github.com/launchdarkly/go-sdk-events/v3 v3.2.0 // indirect + github.com/launchdarkly/go-semver v1.0.2 // indirect + github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + golang.org/x/sys v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/ldotel/go.sum b/ldotel/go.sum new file mode 100644 index 00000000..a7c03822 --- /dev/null +++ b/ldotel/go.sum @@ -0,0 +1,80 @@ +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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f h1:kOkUP6rcVVqC+KlKKENKtgfFfJyDySYhqL9srXooghY= +github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA= +github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003/go.mod h1:zNBxMY8P21owkeogJELCLeHIt+voOSduHYTFUbwRAV8= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/launchdarkly/ccache v1.1.0 h1:voD1M+ZJXR3MREOKtBwgTF9hYHl1jg+vFKS/+VAkR2k= +github.com/launchdarkly/ccache v1.1.0/go.mod h1:TlxzrlnzvYeXiLHmesMuvoZetu4Z97cV1SsdqqBJi1Q= +github.com/launchdarkly/eventsource v1.6.2 h1:5SbcIqzUomn+/zmJDrkb4LYw7ryoKFzH/0TbR0/3Bdg= +github.com/launchdarkly/eventsource v1.6.2/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk= +github.com/launchdarkly/go-jsonstream/v3 v3.0.0 h1:qJF/WI09EUJ7kSpmP5d1Rhc81NQdYUhP17McKfUq17E= +github.com/launchdarkly/go-jsonstream/v3 v3.0.0/go.mod h1:/1Gyml6fnD309JOvunOSfyysWbZ/ZzcA120gF/cQtC4= +github.com/launchdarkly/go-sdk-common/v3 v3.1.0 h1:KNCP5rfkOt/25oxGLAVgaU1BgrZnzH9Y/3Z6I8bMwDg= +github.com/launchdarkly/go-sdk-common/v3 v3.1.0/go.mod h1:mXFmDGEh4ydK3QilRhrAyKuf9v44VZQWnINyhqbbOd0= +github.com/launchdarkly/go-sdk-events/v3 v3.2.0 h1:FUby/4cUSVDghCkFDpvy+7vZlIW4+CK95HjQnuqGXVs= +github.com/launchdarkly/go-sdk-events/v3 v3.2.0/go.mod h1:oepYWQ2RvvjfL2WxkE1uJJIuRsIMOP4WIVgUpXRPcNI= +github.com/launchdarkly/go-semver v1.0.2 h1:sYVRnuKyvxlmQCnCUyDkAhtmzSFRoX6rG2Xa21Mhg+w= +github.com/launchdarkly/go-semver v1.0.2/go.mod h1:xFmMwXba5Mb+3h72Z+VeSs9ahCvKo2QFUTHRNHVqR28= +github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.0 h1:nQbR1xCpkdU9Z71FI28bWTi5LrmtSVURy0UFcBVD5ZU= +github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.0/go.mod h1:cwk7/7SzNB2wZbCZS7w2K66klMLBe3NFM3/qd3xnsRc= +github.com/launchdarkly/go-server-sdk/v7 v7.3.0 h1:blc8npHPjhXGs2NU68YSKby6Xkxp16aDSObLt3W5Qww= +github.com/launchdarkly/go-server-sdk/v7 v7.3.0/go.mod h1:EY2ag+p9HnNXiG4pJ+y7QG2gqCYEoYD+NJgwkhmUUqk= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw= +github.com/launchdarkly/go-test-helpers/v3 v3.0.2 h1:rh0085g1rVJM5qIukdaQ8z1XTWZztbJ49vRZuveqiuU= +github.com/launchdarkly/go-test-helpers/v3 v3.0.2/go.mod h1:u2ZvJlc/DDJTFrshWW50tWMZHLVYXofuSHUfTU/eIwM= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= +golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ldotel/package_info.go b/ldotel/package_info.go new file mode 100644 index 00000000..5561f98e --- /dev/null +++ b/ldotel/package_info.go @@ -0,0 +1,11 @@ +// Package ldotel contains OpenTelemetry specific implementations of hooks. +// +// For instance, to use LaunchDarkly with OpenTelemetry tracing, one would use the TracingHook: +// +// client, _ = ld.MakeCustomClient("sdk-key", ld.Config{ +// Hooks: []ldhooks.Hook{ldotel.NewTracingHook()}, +// }, 5*time.Second) +package ldotel + +// Version is the current version string of the ldotel package. This is updated by our release scripts. +const Version = "0.0.1" // {{ x-release-please-version }} diff --git a/ldotel/tracing_hook.go b/ldotel/tracing_hook.go new file mode 100644 index 00000000..36f2c555 --- /dev/null +++ b/ldotel/tracing_hook.go @@ -0,0 +1,111 @@ +package ldotel + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.18.0" + "go.opentelemetry.io/otel/trace" + + "github.com/launchdarkly/go-sdk-common/v3/ldreason" + "github.com/launchdarkly/go-server-sdk/v7/ldhooks" +) + +const eventName = "feature_flag" +const contextKeyAttributeName = "feature_flag.context.key" + +// TracingHookOption is used to implement functional options for the TracingHook. +type TracingHookOption func(hook *TracingHook) + +// WithSpans is an experimental option that enables creation of child spans for each variation call. +// +// This feature is experimental and the data in the spans, or nesting of spans, could change in future versions. +func WithSpans() TracingHookOption { + return func(h *TracingHook) { + h.spans = true + } +} + +// WithVariant option enables putting a stringified version of the flag value in the feature_flag span event. +func WithVariant() TracingHookOption { + return func(h *TracingHook) { + h.includeVariant = true + } +} + +// A TracingHook adds OpenTelemetry support to the LaunchDarkly SDK. +// +// By default, span events will be added for each call to a "Variation" method. Variation methods without "Ctx" will not +// be able to access a parent span, so no span events can be attached. If WithSpans is used, then root spans will be +// created from the non-"Ctx" methods. +// +// The span event will include the FullyQualifiedKey of the ldcontext, the provider of the evaluation (LaunchDarkly), +// and the key of the flag being evaluated. +type TracingHook struct { + ldhooks.Unimplemented + metadata ldhooks.Metadata + spans bool + includeVariant bool + tracer trace.Tracer +} + +// Metadata returns meta-data about the tracing hook. +func (h TracingHook) Metadata() ldhooks.Metadata { + return h.metadata +} + +// NewTracingHook creates a new TracingHook instance. The TracingHook can be provided to the LaunchDarkly client +// in order to add OpenTelemetry support. +func NewTracingHook(opts ...TracingHookOption) TracingHook { + hook := TracingHook{ + metadata: ldhooks.NewMetadata("LaunchDarkly Tracing Hook"), + tracer: otel.Tracer("launchdarkly-client"), + } + for _, opt := range opts { + opt(&hook) + } + return hook +} + +// BeforeEvaluation implements the BeforeEvaluation evaluation stage. +func (h TracingHook) BeforeEvaluation(ctx context.Context, seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { + if h.spans { + _, span := h.tracer.Start(ctx, seriesContext.Method()) + + span.SetAttributes(semconv.FeatureFlagKey(seriesContext.FlagKey()), + attribute.String(contextKeyAttributeName, seriesContext.Context().FullyQualifiedKey())) + + return ldhooks.NewEvaluationSeriesBuilder(data).Set("variationSpan", span).Build(), nil + } + return data, nil +} + +// AfterEvaluation implements the AfterEvaluation evaluation stage. +func (h TracingHook) AfterEvaluation(ctx context.Context, seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, detail ldreason.EvaluationDetail) (ldhooks.EvaluationSeriesData, error) { + variationSpan, present := data.Get("variationSpan") + if present { + asSpan, ok := variationSpan.(trace.Span) + if ok { + asSpan.End() + } + } + + attribs := []attribute.KeyValue{ + semconv.FeatureFlagKey(seriesContext.FlagKey()), + semconv.FeatureFlagProviderName("LaunchDarkly"), + attribute.String(contextKeyAttributeName, seriesContext.Context().FullyQualifiedKey()), + } + if h.includeVariant { + attribs = append(attribs, semconv.FeatureFlagVariant(detail.Value.JSONString())) + } + + span := trace.SpanFromContext(ctx) + span.AddEvent(eventName, trace.WithAttributes(attribs...)) + return data, nil +} + +// Ensure that TracingHook conforms to the ldhooks.Hook interface. +var _ ldhooks.Hook = TracingHook{} diff --git a/ldotel/tracing_hook_test.go b/ldotel/tracing_hook_test.go new file mode 100644 index 00000000..13ab2aef --- /dev/null +++ b/ldotel/tracing_hook_test.go @@ -0,0 +1,171 @@ +package ldotel + +import ( + gocontext "context" + "testing" + + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + ldclient "github.com/launchdarkly/go-server-sdk/v7" + "github.com/launchdarkly/go-server-sdk/v7/ldhooks" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/trace" +) +import "go.opentelemetry.io/otel/sdk/trace/tracetest" + +func configureMemoryExporter() *tracetest.InMemoryExporter { + exporter := tracetest.NewInMemoryExporter() + sp := trace.NewSimpleSpanProcessor(exporter) + provider := trace.NewTracerProvider( + trace.WithSpanProcessor(sp), + ) + otel.SetTracerProvider(provider) + return exporter +} + +func createClientWithTracing(options ...TracingHookOption) *ldclient.LDClient { + client, _ := ldclient.MakeCustomClient("", ldclient.Config{ + Offline: true, + Hooks: []ldhooks.Hook{NewTracingHook(options...)}, + }, 0) + return client +} + +const flagKey = "test-flag" +const spanName = "test-span" + +func TestBasicSpanEventsEvents(t *testing.T) { + exporter := configureMemoryExporter() + tracer := otel.Tracer("launchdarkly-client") + client := createClientWithTracing() + context := ldcontext.New("test-context") + + ctx := gocontext.Background() + + ctx, span := tracer.Start(ctx, spanName) + + _, _ = client.BoolVariationCtx(ctx, flagKey, context, false) + + span.End() + + exportedSpans := exporter.GetSpans().Snapshots() + assert.Len(t, exportedSpans, 1) + events := exportedSpans[0].Events() + assert.Len(t, events, 1) + flagEvent := events[0] + assert.Equal(t, "feature_flag", flagEvent.Name) + + attributes := attribute.NewSet(flagEvent.Attributes...) + attributeFlagKey, _ := (&attributes).Value("feature_flag.key") + assert.Equal(t, flagKey, attributeFlagKey.AsString()) + attributeProviderName, _ := (&attributes).Value("feature_flag.provider_name") + assert.Equal(t, "LaunchDarkly", attributeProviderName.AsString()) + attributeContextKey, _ := (&attributes).Value("feature_flag.context.key") + assert.Equal(t, context.FullyQualifiedKey(), attributeContextKey.AsString()) +} + +func TestSpanEventsWithVariant(t *testing.T) { + exporter := configureMemoryExporter() + tracer := otel.Tracer("launchdarkly-client") + client := createClientWithTracing(WithVariant()) + context := ldcontext.New("test-context") + + ctx := gocontext.Background() + + ctx, span := tracer.Start(ctx, spanName) + + _, _ = client.BoolVariationCtx(ctx, flagKey, context, false) + + span.End() + + exportedSpans := exporter.GetSpans().Snapshots() + events := exportedSpans[0].Events() + flagEvent := events[0] + + attributes := attribute.NewSet(flagEvent.Attributes...) + attributeVariant, _ := (&attributes).Value("feature_flag.variant") + assert.Equal(t, "false", attributeVariant.AsString()) +} + +func TestMultipleSpanEvents(t *testing.T) { + exporter := configureMemoryExporter() + tracer := otel.Tracer("launchdarkly-client") + client := createClientWithTracing() + context := ldcontext.New("test-context") + + ctx := gocontext.Background() + + ctx, span := tracer.Start(ctx, spanName) + + _, _ = client.BoolVariationCtx(ctx, flagKey, context, false) + _, _ = client.StringVariationCtx(ctx, flagKey, context, "default") + + span.End() + + exportedSpans := exporter.GetSpans().Snapshots() + assert.Len(t, exportedSpans, 1) + events := exportedSpans[0].Events() + assert.Len(t, events, 2) + flagEventBool := events[0] + assert.Equal(t, "feature_flag", flagEventBool.Name) + + boolFlagEventAttributes := attribute.NewSet(flagEventBool.Attributes...) + boolAttributeFlagKey, _ := (&boolFlagEventAttributes).Value("feature_flag.key") + assert.Equal(t, flagKey, boolAttributeFlagKey.AsString()) + boolAttributeProviderName, _ := (&boolFlagEventAttributes).Value("feature_flag.provider_name") + assert.Equal(t, "LaunchDarkly", boolAttributeProviderName.AsString()) + boolAttributeContextKey, _ := (&boolFlagEventAttributes).Value("feature_flag.context.key") + assert.Equal(t, context.FullyQualifiedKey(), boolAttributeContextKey.AsString()) + + flagEventString := events[1] + assert.Equal(t, "feature_flag", flagEventString.Name) + + stringFlagEventAttributes := attribute.NewSet(flagEventString.Attributes...) + stringAttributeFlagKey, _ := (&stringFlagEventAttributes).Value("feature_flag.key") + assert.Equal(t, flagKey, stringAttributeFlagKey.AsString()) + stringAttributeProviderName, _ := (&boolFlagEventAttributes).Value("feature_flag.provider_name") + assert.Equal(t, "LaunchDarkly", stringAttributeProviderName.AsString()) + stringAttributeContextKey, _ := (&stringFlagEventAttributes).Value("feature_flag.context.key") + assert.Equal(t, context.FullyQualifiedKey(), stringAttributeContextKey.AsString()) +} + +func TestSpanCreationWithParent(t *testing.T) { + exporter := configureMemoryExporter() + tracer := otel.Tracer("launchdarkly-client") + client := createClientWithTracing(WithSpans()) + context := ldcontext.New("test-context") + + ctx := gocontext.Background() + + ctx, span := tracer.Start(ctx, spanName) + + _, _ = client.BoolVariationCtx(ctx, flagKey, context, false) + + span.End() + + exportedSpans := exporter.GetSpans().Snapshots() + assert.Len(t, exportedSpans, 2) + + exportedSpan := exportedSpans[0] + assert.Equal(t, "LDClient.BoolVariationCtx", exportedSpan.Name()) + + attributes := attribute.NewSet(exportedSpan.Attributes()...) + attributeFlagKey, _ := (&attributes).Value("feature_flag.key") + assert.Equal(t, flagKey, attributeFlagKey.AsString()) + attributeContextKey, _ := (&attributes).Value("feature_flag.context.key") + assert.Equal(t, context.FullyQualifiedKey(), attributeContextKey.AsString()) +} + +func TestSpanCreationWithoutParent(t *testing.T) { + exporter := configureMemoryExporter() + client := createClientWithTracing(WithSpans()) + context := ldcontext.New("test-context") + + _, _ = client.BoolVariation(flagKey, context, false) + + exportedSpans := exporter.GetSpans().Snapshots() + assert.Len(t, exportedSpans, 1) + exportedSpan := exportedSpans[0] + assert.Equal(t, "LDClient.BoolVariation", exportedSpan.Name()) +} diff --git a/release-please-config.json b/release-please-config.json index 0bfafea5..cf85ca1b 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,5 +1,6 @@ { "packages" : { + "separate-pull-requests": true, "." : { "release-type" : "go", "bump-minor-pre-major" : true, @@ -9,6 +10,16 @@ "extra-files" : [ "internal/version.go" ] + }, + "ldotel" : { + "release-type" : "go", + "bump-minor-pre-major" : true, + "tag-separator": "/", + "versioning" : "default", + "include-component-in-tag" : true, + "extra-files" : [ + "internal/version.go" + ] } } }