├── .github ├── dependabot.yml └── workflows │ ├── dependabot.yml │ ├── go.yml │ └── repolinter.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── THIRD_PARTY_NOTICES.md ├── examples └── simple │ ├── LICENSE.txt │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── screenshot01.png │ └── screenshot02.png ├── go.mod ├── go.sum └── newrelic ├── example_test.go ├── exporter.go ├── exporter_test.go └── internal └── transform ├── identifiers.go ├── metric.go ├── metric_test.go ├── span.go └── span_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | # Workflow files stored in the 10 | # default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "sunday" 15 | - package-ecosystem: "gomod" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | day: "sunday" 20 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot-Tidier 2 | on: 3 | pull_request: 4 | types: [ labeled ] 5 | 6 | jobs: 7 | mod_tidier: 8 | if: ${{ contains(github.event.pull_request.labels.*.name, 'dependencies') }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: '^1.14.0' 17 | - uses: evantorrie/mott-the-tidier@v1-beta 18 | id: modtidy 19 | with: 20 | gomods: '**/go.mod' 21 | gomodsum_only: true 22 | - uses: stefanzweifel/git-auto-commit-action@v4 23 | id: autocommit 24 | with: 25 | commit_message: Auto-fix go.sum changes in dependent modules 26 | - name: changes 27 | run: | 28 | echo "Changes detected: ${{ steps.autocommit.outputs.changes_detected }}" 29 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | 8 | go-otel-exporter: 9 | runs-on: ubuntu-18.04 10 | 11 | strategy: 12 | # if one test fails, do not abort the rest 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - go-version: 1.15.x 17 | - go-version: 1.16.x 18 | steps: 19 | 20 | - name: Set up Go 1.x 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v2 28 | 29 | - name: Build 30 | run: go build -v ./... 31 | 32 | - name: Test 33 | run: go test -v ./... 34 | 35 | - name: Vet 36 | run: go vet ./... 37 | -------------------------------------------------------------------------------- /.github/workflows/repolinter.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 New Relic Corporation. All rights reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # NOTE: This file should always be named `repolinter.yml` to allow 5 | # workflow_dispatch to work properly 6 | name: Repolinter Action 7 | 8 | # NOTE: This workflow will ONLY check the default branch! 9 | # Currently there is no elegant way to specify the default 10 | # branch in the event filtering, so branches are instead 11 | # filtered in the "Test Default Branch" step. 12 | on: [push, workflow_dispatch] 13 | 14 | jobs: 15 | repolint: 16 | name: Run Repolinter 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Test Default Branch 20 | id: default-branch 21 | uses: actions/github-script@v4.0.2 22 | with: 23 | script: | 24 | const data = await github.repos.get(context.repo) 25 | return data.data && data.data.default_branch === context.ref.split('/').slice(-1)[0] 26 | - name: Checkout Self 27 | if: ${{ steps.default-branch.outputs.result == 'true' }} 28 | uses: actions/checkout@v2 29 | - name: Run Repolinter 30 | if: ${{ steps.default-branch.outputs.result == 'true' }} 31 | uses: newrelic/repolinter-action@v1 32 | with: 33 | config_url: https://raw.githubusercontent.com/newrelic/.github/main/repolinter-rulesets/community-plus.yml 34 | output_type: issue 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.20.0] - 2021-05-26 10 | 11 | ### Changed 12 | - Added archival notice to README. In the near future, this exporter will no longer be maintained in favor of using [New Relic's native OTLP ingest](https://docs.newrelic.com/whats-new/2021/04/native-support-opentelemetry/). It is suggested to use the [OpenTelemetry Go OTLP exporter](https://github.com/open-telemetry/opentelemetry-go/tree/main/exporters/otlp). 13 | - Upgraded `go.opentelemetry.io/otel*` dependencies to v0.20.0. ([#92](https://github.com/newrelic/opentelemetry-exporter-go/pull/92)) 14 | 15 | ## [0.18.0] - 2021-04-06 16 | 17 | ### Changed 18 | - Upgraded `go.opentelemetry.io/otel*` dependencies to v0.19.0. Thanks to @akulnurislam for getting us to v0.18.0! ([#71](https://github.com/newrelic/opentelemetry-exporter-go/pull/71), [#74](https://github.com/newrelic/opentelemetry-exporter-go/pull/74)) 19 | 20 | ### Added 21 | - Clarified language and examples in the sample application for customers using 22 | New Relic's EU datacenter. 23 | 24 | ## [0.17.0] - 2021-03-04 25 | 26 | ### Changed 27 | 28 | - Upgrade `go.opentelemetry.io/otel*` to v0.17.0 and `github.com/newrelic/newrelic-telemetry-sdk-go` to v0.5.2. 29 | ([#63](https://github.com/newrelic/opentelemetry-exporter-go/pull/63)) 30 | - Added SpanKind to Getting Started guide and simple sample application in order 31 | to provide a better New Relic UI experience. 32 | ([#54](https://github.com/newrelic/opentelemetry-exporter-go/pull/54)) 33 | 34 | ## [0.15.1] - 2021-01-26 35 | 36 | ### Changed 37 | 38 | - Upgraded `go.opentelemetry.io/otel*` dependencies to v0.16.0. ([#48](https://github.com/newrelic/opentelemetry-exporter-go/pull/48)) 39 | 40 | ### Added 41 | 42 | - Added Getting Started guide with sample application. ([#44](https://github.com/newrelic/opentelemetry-exporter-go/pull/44), [#49](https://github.com/newrelic/opentelemetry-exporter-go/pull/49)) 43 | 44 | ## [0.14.0] - 2020-12-04 45 | 46 | ### Changed 47 | 48 | - Upgrade `go.opentelemetry.io/otel*` to v0.14.0. ([#40](https://github.com/newrelic/opentelemetry-exporter-go/pull/40)) 49 | 50 | ## [0.13.0] - 2020-10-28 51 | 52 | ### Added 53 | 54 | - Support for metrics (#10) 55 | - Version number has been modified to track the version numbers of the 56 | go.opentelemetry.io/otel upstream library. 57 | 58 | ### Changed 59 | 60 | - Updated to use version 0.13.0 of the go.opentelemetry.io/otel packages. (#30) 61 | - Standardized CHANGELOG.md format. When making changes to this project, add 62 | human-readable summaries of what you've done to the "Unreleased" section 63 | above. When creating a release, move that information into a new release 64 | section in this document. (#35) 65 | 66 | ## [0.1.0] - 2019-12-31 67 | 68 | First release! 69 | 70 | [Unreleased]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.20.0...HEAD 71 | [0.20.0]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.18.0...v0.20.0 72 | [0.18.0]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.17.0...v0.18.0 73 | [0.17.0]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.15.1...v0.17.0 74 | [0.15.1]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.14.0...v0.15.1 75 | [0.14.0]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.13.0...v0.14.0 76 | [0.13.0]: https://github.com/newrelic/opentelemetry-exporter-go/compare/v0.1.0...v0.13.0 77 | [0.1.0]: https://github.com/newrelic/opentelemetry-exporter-go/releases/tag/v0.1.0 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the New Relic Go OpenTelemetry Exporter 2 | Thanks for your interest in contributing to the New Relic Go OpenTelemetry Exporter! We look forward to engaging with you. 3 | 4 | ## How to contribute 5 | * Read this CONTRIBUTING file 6 | * Read our [Code of Conduct](CODE_OF_CONDUCT.md) 7 | * Submit a [pull request](#pull-request-guidelines) or [issue](#filing-issues--bug-reports). For pull requests, please also ensure that your work satisfies: 8 | * Unit tests (`go test ./...`) 9 | * [golint](https://github.com/golang/lint) 10 | * [go vet](https://golang.org/cmd/vet/) 11 | * [go fmt](https://golang.org/cmd/gofmt/) 12 | * Ensure you’ve signed the CLA, otherwise you’ll be asked to do so. 13 | 14 | ## How to get help or ask questions 15 | Do you have questions or are you experiencing unexpected behaviors after modifying this Open Source Software? Please engage with the “Build on New Relic” space in the [Explorers Hub](https://discuss.newrelic.com/c/build-on-new-relic/Open-Source-Agents-SDKs), New Relic’s Forum. Posts are publicly viewable by anyone, please do not include PII or sensitive information in your forum post. 16 | 17 | ## Contributor License Agreement ("CLA") 18 | 19 | We'd love to get your contributions to improve the New Relic Go OpenTelemetry Exporter! Keep in mind when you submit your pull request, you'll need to sign the CLA via the click-through using CLA-Assistant. You only have to sign the CLA one time per project. 20 | 21 | To execute our corporate CLA, which is required if your contribution is on behalf of a company, or if you have any questions, please drop us an email at open-source@newrelic.com. 22 | 23 | ## Filing Issues & Bug Reports 24 | We use GitHub issues to track public issues and bugs. If possible, please provide a link to an example app or gist that reproduces the issue. When filing an issue, please ensure your description is clear and includes the following information. Be aware that GitHub issues are publicly viewable by anyone, so please do not include personal information in your GitHub issue or in any of your contributions, except as minimally necessary for the purpose of supporting your issue. New Relic will process any personal data you submit through GitHub issues in compliance with the [New Relic Privacy Notice](https://newrelic.com/termsandconditions/privacy). 25 | - Project version (ex: 0.4.0) 26 | - Custom configurations (ex: flag=true) 27 | - Any modifications made to the exporter 28 | 29 | ### A note about vulnerabilities 30 | New Relic is committed to the privacy and security of our customers and their data. We believe that providing coordinated disclosure by security researchers and engaging with the security community are important means to achieve our security goals. 31 | 32 | If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [HackerOne](https://hackerone.com/newrelic). 33 | 34 | ## Setting up your environment 35 | This Open Source Software can be used in a large number of environments, all of which have their own quirks and best practices. As such, while we are happy to provide documentation and assistance for unmodified Open Source Software, we cannot provide support for your specific environment or your modifications to the code. 36 | 37 | ## Pull Request Guidelines 38 | Before we can accept a pull request, you must sign our [Contributor Licensing Agreement](#contributor-license-agreement-cla), if you have not already done so. This grants us the right to use your code under the same Apache 2.0 license as we use for this project in general. 39 | 40 | If this is a notable change, please include a very short summary of your work in the "Unreleased" section of [CHANGELOG.md](./CHANGELOG.MD). 41 | 42 | ## Coding Style Guidelines 43 | Our code base is formatted according to [gofmt](https://golang.org/cmd/gofmt/) and linted with [golint](https://github.com/golang/lint). 44 | 45 | ## License 46 | By contributing to the New Relic Go OpenTelemetry Exporter, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 2019 New Relic, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Archive header](https://github.com/newrelic/opensource-website/raw/master/src/images/categories/Archived.png)](https://opensource.newrelic.com/oss-category/#archived) 2 | 3 | # Archival Notice 4 | 5 | ❗Notice: This project has been archived _as is_ and is no longer actively maintained. 6 | 7 | Rather than developing a Go specific OpenTelemetry exporter New Relic is now offering native OTLP ingest. 8 | 9 | The current recommended approaches for sending OpenTelemetry data to the New Relic is to configure your OpenTelemetry data source to send data to the native OpenTelemetry Protocol (OTLP) data ingestion endpoint. [OTLP](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md) is an open source gRPC based protocol for sending telemetry data. The protocol is vendor agnostic and open source. Applications can export data directly to New Relic via OTLP, or first to the [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) and then on to New Relic via OTLP. 10 | 11 | For more details please see: 12 | * [OpenTelemetry quick start](https://docs.newrelic.com/docs/integrations/open-source-telemetry-integrations/opentelemetry/opentelemetry-quick-start/) 13 | * [Introduction to OpenTelemetry with New Relic](https://docs.newrelic.com/docs/integrations/open-source-telemetry-integrations/opentelemetry/introduction-opentelemetry-new-relic/) 14 | 15 | --- 16 | 17 | # New Relic Go OpenTelemetry exporter [![GoDoc](https://godoc.org/github.com/newrelic/opentelemetry-exporter-go/newrelic?status.svg)](https://godoc.org/github.com/newrelic/opentelemetry-exporter-go/newrelic) 18 | 19 | The `"github.com/newrelic/opentelemetry-exporter-go/newrelic"` package 20 | provides an exporter for sending OpenTelemetry data to New Relic. Currently, 21 | traces and the latest metric instruments (as of v0.19 of Open Telemetry for Go) are 22 | supported. 23 | 24 | 25 | ## **Getting Started** 26 | 27 | ### **Go OpenTelemetry** 28 | 29 | OpenTelemetry is a set of vendor-neutral tools, APIs, SDKs, and an optional collector service for capturing distributed traces and metrics from your application. New Relic’s Go OpenTelemetry Exporter makes it easy for you to export your captured data to your New Relic account. 30 | 31 | To help you get started, this guide shows you how to set up a tiny Go application to send spans and metrics to New Relic. If you have an application that already uses an existing framework like [gin](https://github.com/gin-gonic/gin) or [beego](https://beego.me/), it’s worth your time to go through the steps below, and then check out the [many auto-instrumentation options in the opentelemetry-go-contrib repository](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/master/instrumentation#instrumentation). 32 | 33 | ### **Get Started** 34 | Here’s what you need to get started exporting OpenTelemetry spans and metrics to New Relic: 35 | * Make sure you have signed up for a [New Relic account](https://docs.newrelic.com/docs/accounts/accounts-billing/account-setup/create-your-new-relic-account). 36 | * Obtain an [Insights Event Insert API Key](https://docs.newrelic.com/docs/telemetry-data-platform/ingest-manage-data/ingest-apis/use-event-api-report-custom-events#) to send spans and metrics to New Relic. (In that guide, you only need to proceed as far as obtaining a new key, the rest of the steps are optional.) 37 | 38 | At this point, you have a couple of alternatives: 39 | * If you just want to see it working quickly, we have a tiny sample application set up for you [right here](examples/simple), already set up to export data to your New Relic account. Once you have it checked out, skip to [Running the sample application](#Running-the-sample-application) in this document. 40 | * If you’re starting from scratch, review the Go OpenTelemetry [Getting Started Guide](https://opentelemetry.io/docs/go/getting-started/). This excellent guide will walk you through creating a tiny sample application that generates spans and metrics, and outputs text to your terminal. Then [modify your sample application](#Modify-the-OpenTelemetry-sample-application) to send data to New Relic using the instructions below. 41 | 42 | Lastly, [view your data in the New Relic One UI](#View-your-data-in-the-New-Relic-One-UI). Very satisfying! 43 | 44 | ### **Modify the OpenTelemetry sample application** 45 | Here’s what to do to switch out the text-based exporter defined in the Go OpenTelemetry SIG’s [Getting Started Guide](https://opentelemetry.io/docs/go/getting-started/) with the New Relic OpenTelemetry Exporter for Go. 46 | 47 | There are three steps to get it reporting to New Relic: 48 | 49 | 1. replace an import statement, and add some imports 50 | 1. instantiate a ```newrelic``` exporter with some configuration options 51 | 1. set the span.kind for a better UI experience in New Relic 52 | 53 | 54 | Full source of this modified sample application is available in examples/simple/main.go. 55 | 56 | 1. Replace the exporter import clause. Switch this: 57 | 58 | ```go 59 | "go.opentelemetry.io/otel/exporters/stdout" 60 | ``` 61 | 62 | ...with this: 63 | 64 | ```go 65 | "github.com/newrelic/opentelemetry-exporter-go/newrelic" 66 | ``` 67 | 68 | You'll also need to add some imports, if they're missing: 69 | ``` 70 | "os" 71 | "fmt" 72 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 73 | ``` 74 | 75 | 76 | 2. Rather than instantiate a ```stdout``` exporter, instantiate a ```newrelic``` exporter. Replace this: 77 | 78 | ```go 79 | exporter, err := stdout.NewExporter( 80 | stdout.WithPrettyPrint(), 81 | ) 82 | if err != nil { 83 | log.Fatalf("failed to initialize stdout export pipeline: %v", err) 84 | } 85 | ctx := context.Background() 86 | ``` 87 | ...with this: 88 | 89 | ```go 90 | apiKey, ok := os.LookupEnv("NEW_RELIC_API_KEY") 91 | if !ok { 92 | fmt.Println("Missing NEW_RELIC_API_KEY required for New Relic OpenTelemetry Exporter") 93 | os.Exit(1) 94 | } 95 | 96 | exporter, err := newrelic.NewExporter( 97 | "Simple OpenTelemetry Service", 98 | apiKey, 99 | telemetry.ConfigBasicErrorLogger(os.Stderr), 100 | telemetry.ConfigBasicDebugLogger(os.Stderr), 101 | telemetry.ConfigBasicAuditLogger(os.Stderr), 102 | ) 103 | if err != nil { 104 | fmt.Printf("Failed to instantiate New Relic OpenTelemetry exporter: %v\n", err) 105 | os.Exit(1) 106 | } 107 | 108 | ctx := context.Background() 109 | defer exporter.Shutdown(ctx) 110 | ``` 111 | 112 | There are four things to notice above: 113 | 114 | * The configuration reads the [Insights Insert Key](https://docs.newrelic.com/docs/telemetry-data-platform/ingest-manage-data/ingest-apis/use-event-api-report-custom-events#) you obtained earlier from an environment variable called ```NEW_RELIC_API_KEY```. Keep this key safe from others, since it’s both identification and authentication in one handy token. 115 | 116 | * This exporter uses ```Simple OpenTelemetry Service``` as its service name. This is the name you will see in the New Relic One UI. 117 | 118 | * The configuration turns on all the logging it can for the purposes of this demo. To have a more silent exporter with lower overhead, remove those three lines. 119 | 120 | * Once we have the context (```ctx```), we defer the Shutdown function so the exporter has a chance to flush any accumulated data to the New Relic [Metrics and Traces](https://newrelic.com/platform/telemetry-data-101) endpoints. 121 | 122 | 3. This example generates a parent span and a child span. For the parent, set 123 | the kind of span to "server" to get the best experience in the New Relic UI. 124 | 125 | Change: 126 | 127 | ``` 128 | ctx, span = tracer.Start(ctx, "operation") 129 | ``` 130 | ...to: 131 | 132 | ``` 133 | ctx, span = tracer.Start(ctx, "operation", 134 | trace.WithSpanKind(trace.SpanKindServer)) 135 | ``` 136 | 137 | You’re now set! If you’re not using go mod, you’ll need to download the exporter using the go get command: 138 | 139 | ``` 140 | go get github.com/newrelic/opentelemetry-exporter-go 141 | ``` 142 | 143 | ### **Running the sample application** 144 | To see it in action, just use go run with the [Insights Insert API Key](https://docs.newrelic.com/docs/telemetry-data-platform/ingest-manage-data/ingest-apis/use-event-api-report-custom-events#) set in the environment: 145 | 146 | ``` 147 | NEW_RELIC_API_KEY=NRII-REPLACE_THIS_KEY run main.go 148 | ``` 149 | 150 | Note that the key above isn’t a valid key -- replace it with your own. 151 | 152 | Run that line a couple of times to send some data into New Relic. 153 | 154 | ### **View your data in the New Relic One UI** 155 | 156 | **To see span data** 157 | 158 | Go to the [New Relic One UI](https://one.newrelic.com) in your web browser, click the magnifying glass in the upper right corner and type your service name, which is ```Simple OpenTelemetry Service``` if you didn't change it from the example. New Relic searches through everything your user has permissions to see and will show you a link to the application. If you’re using a web framework, you’ll see a Summary overview containing response time, throughput, and errors. To see just the spans, click the “Distributed Tracing” header on the left. 159 | 160 | ![Image of Span Data in New Relic One](examples/simple/screenshot01.png) 161 | 162 | **To see metric data** 163 | 164 | Go to the [New Relic One UI](https://one.newrelic.com) in your web browser, click the "Query your data" link in the upper right corner, select your account, and then try this query: 165 | SELECT * FROM Metric where entity.name like 'Simple OpenTelemetry Service' 166 | 167 | 168 | ![Image of Span Data in New Relic One](examples/simple/screenshot02.png) 169 | 170 | For more tips on how to find and query your data in New Relic, see [Find trace/span data](https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/trace-api/introduction-trace-api#view-data). 171 | 172 | For general querying information, see: 173 | - [Query New Relic data](https://docs.newrelic.com/docs/using-new-relic/data/understand-data/query-new-relic-data) 174 | - [Intro to NRQL](https://docs.newrelic.com/docs/query-data/nrql-new-relic-query-language/getting-started/introduction-nrql) 175 | 176 | ## **This exporter is built on alpha software** 177 | 178 | This exporter is built with the alpha release of OpenTelemetry Go client. Due 179 | to the rapid development of OpenTelemetry, this exporter does not guarantee 180 | compatibility with future releases of the OpenTelemetry APIs. Additionally, 181 | this exporter may introduce changes that are not backwards compatible without a 182 | major version increment. We will strive to ensure backwards compatibility when 183 | a stable version of the OpenTelemetry Go client is released. 184 | 185 | 186 | ## **Support** 187 | 188 | Should you need assistance with New Relic products, you are in good hands with several support channels. 189 | 190 | If the issue has been confirmed as a bug or is a feature request, file a GitHub issue. 191 | 192 | **Support Channels** 193 | 194 | * [New Relic Documentation](https://docs.newrelic.com/docs/integrations/open-source-telemetry-integrations/opentelemetry/introduction-opentelemetry-new-relic/): Comprehensive guidance for using our platform 195 | * [New Relic Community](https://discuss.newrelic.com/tag/goagent): The best place to engage in troubleshooting questions 196 | * [New Relic Developer](https://developer.newrelic.com/): Resources for building a custom observability applications 197 | * [New Relic University](https://learn.newrelic.com/): A range of online training for New Relic users of every level 198 | 199 | 200 | ## **Privacy** 201 | 202 | At New Relic we take your privacy and the security of your information seriously, and are committed to protecting your information. We must emphasize the importance of not sharing personal data in public forums, and ask all users to scrub logs and diagnostic information for sensitive information, whether personal, proprietary, or otherwise. 203 | 204 | We define “Personal Data” as any information relating to an identified or identifiable individual, including, for example, your name, phone number, post code or zip code, Device ID, IP address, and email address. 205 | 206 | For more information, review [New Relic’s General Data Privacy Notice](https://newrelic.com/termsandconditions/privacy). 207 | 208 | 209 | ## **Contribute** 210 | 211 | We encourage your contributions to improve opentelemetry-exporter-go! Keep in mind that when you submit your pull request, you'll need to sign the CLA via the click-through using CLA-Assistant. You only have to sign the CLA one time per project. 212 | 213 | If you have any questions, or to execute our corporate CLA (which is required if your contribution is on behalf of a company), drop us an email at opensource@newrelic.com. 214 | 215 | **A note about vulnerabilities** 216 | 217 | As noted in our [security policy](../../security/policy), New Relic is committed to the privacy and security of our customers and their data. We believe that providing coordinated disclosure by security researchers and engaging with the security community are important means to achieve our security goals. 218 | 219 | If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [HackerOne](https://hackerone.com/newrelic). 220 | 221 | If you would like to contribute to this project, review [these guidelines](./CONTRIBUTING.md). 222 | 223 | To [all contributors](), we thank you! Without your contribution, this project would not be what it is today. We also host a community project page dedicated to [Project Name](). 224 | 225 | 226 | ## **License** 227 | 228 | opentelemetry-exporter-go is licensed under the [Apache 2.0](http://apache.org/licenses/LICENSE-2.0.txt) License. 229 | 230 | 231 | ## **Limitations** 232 | 233 | The New Relic Telemetry APIs are rate limited. Please reference the 234 | documentation for [New Relic Metrics 235 | API](https://docs.newrelic.com/docs/introduction-new-relic-metric-api) and [New 236 | Relic Trace API requirements and 237 | limits](https://docs.newrelic.com/docs/apm/distributed-tracing/trace-api/trace-api-general-requirements-limits) 238 | on the specifics of the rate limits. 239 | -------------------------------------------------------------------------------- /THIRD_PARTY_NOTICES.md: -------------------------------------------------------------------------------- 1 | # Third Party Notices 2 | 3 | The New Relic OpenTelemetry Exporter does not use source code from third party libraries 4 | which carry their own copyright notices and license terms. 5 | 6 | In the event that a required notice is missing or incorrect, please 7 | notify us by e-mailing open-source@newrelic.com. 8 | -------------------------------------------------------------------------------- /examples/simple/LICENSE.txt: -------------------------------------------------------------------------------- 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 2019 New Relic, Inc. 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 | -------------------------------------------------------------------------------- /examples/simple/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/newrelic/opentelemetry-exporter-go/examples/simple 2 | 3 | go 1.15 4 | 5 | replace github.com/newrelic/opentelemetry-exporter-go => ../../ 6 | 7 | require ( 8 | github.com/newrelic/newrelic-telemetry-sdk-go v0.7.1 9 | github.com/newrelic/opentelemetry-exporter-go v0.18.0 10 | go.opentelemetry.io/otel v0.20.0 11 | go.opentelemetry.io/otel/metric v0.20.0 12 | go.opentelemetry.io/otel/sdk v0.20.0 13 | go.opentelemetry.io/otel/sdk/metric v0.20.0 14 | go.opentelemetry.io/otel/trace v0.20.0 15 | ) 16 | -------------------------------------------------------------------------------- /examples/simple/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= 2 | github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 6 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/newrelic/newrelic-telemetry-sdk-go v0.7.1 h1:QXmERkem5rwMxrHPVHogvuflpRaw72Lnfl/knHuyQ0E= 8 | github.com/newrelic/newrelic-telemetry-sdk-go v0.7.1/go.mod h1:2kY6OeOxrJ+RIQlVjWDc/pZlT3MIf30prs6drzMfJ6E= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 13 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= 15 | go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= 16 | go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= 17 | go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= 18 | go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= 19 | go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= 20 | go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8= 21 | go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= 22 | go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g= 23 | go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= 24 | go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8= 25 | go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= 26 | go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= 27 | go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= 28 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 29 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // This example script is the sample from the OpenTelemetry Go "Getting Started" 5 | // guide, with the text-based exporter replaced with the New Relic OpenTelemetry 6 | // Exporter. 7 | 8 | // This example allows customers to override the Metrics and Spans endpoint URLs 9 | // with these environment variables: 10 | // NEW_RELIC_METRIC_URL 11 | // NEW_RELIC_TRACE_URL 12 | 13 | // For example, as of this writing, if using this in the EU, set these two 14 | // environment variables to send data to the New Relic EU datacenter: 15 | // NEW_RELIC_METRIC_URL=https://metric-api.eu.newrelic.com/trace/v1 16 | // NEW_RELIC_TRACE_URL=https://trace-api.eu.newrelic.com/trace/v1 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "log" 24 | "os" 25 | 26 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 27 | "github.com/newrelic/opentelemetry-exporter-go/newrelic" 28 | "go.opentelemetry.io/otel" 29 | "go.opentelemetry.io/otel/attribute" 30 | "go.opentelemetry.io/otel/baggage" 31 | "go.opentelemetry.io/otel/metric" 32 | "go.opentelemetry.io/otel/metric/global" 33 | "go.opentelemetry.io/otel/propagation" 34 | controller "go.opentelemetry.io/otel/sdk/metric/controller/basic" 35 | processor "go.opentelemetry.io/otel/sdk/metric/processor/basic" 36 | "go.opentelemetry.io/otel/sdk/metric/selector/simple" 37 | "go.opentelemetry.io/otel/sdk/resource" 38 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 39 | "go.opentelemetry.io/otel/semconv" 40 | "go.opentelemetry.io/otel/trace" 41 | ) 42 | 43 | func main() { 44 | 45 | // Create a New Relic OpenTelemetry Exporter 46 | apiKey, ok := os.LookupEnv("NEW_RELIC_API_KEY") 47 | if !ok { 48 | fmt.Println("Missing NEW_RELIC_API_KEY required for New Relic OpenTelemetry Exporter") 49 | os.Exit(1) 50 | } 51 | 52 | serviceName := "Simple OpenTelemetry Service" 53 | exporter, err := newrelic.NewExporter( 54 | serviceName, 55 | apiKey, 56 | telemetry.ConfigBasicErrorLogger(os.Stderr), 57 | telemetry.ConfigBasicDebugLogger(os.Stderr), 58 | telemetry.ConfigBasicAuditLogger(os.Stderr), 59 | func(cfg *telemetry.Config) { 60 | cfg.MetricsURLOverride = os.Getenv("NEW_RELIC_METRIC_URL") 61 | cfg.SpansURLOverride = os.Getenv("NEW_RELIC_TRACE_URL") 62 | cfg.EventsURLOverride = os.Getenv("NEW_RELIC_EVENT_URL") 63 | }, 64 | ) 65 | if err != nil { 66 | fmt.Printf("Failed to instantiate New Relic OpenTelemetry exporter: %v\n", err) 67 | os.Exit(1) 68 | } 69 | 70 | ctx := context.Background() 71 | defer exporter.Shutdown(ctx) 72 | 73 | // Minimally default resource with a service name 74 | r := resource.NewWithAttributes(semconv.ServiceNameKey.String(serviceName)) 75 | 76 | // Create a tracer provider 77 | bsp := sdktrace.NewBatchSpanProcessor(exporter) 78 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(bsp), sdktrace.WithResource(r)) 79 | defer func() { _ = tp.Shutdown(ctx) }() 80 | 81 | // Create a meter provider 82 | pusher := controller.New( 83 | processor.New( 84 | simple.NewWithExactDistribution(), 85 | exporter, 86 | ), 87 | controller.WithExporter(exporter), 88 | ) 89 | 90 | err = pusher.Start(ctx) 91 | if err != nil { 92 | log.Fatalf("failed to initialize metric controller: %v", err) 93 | } 94 | pusher.Start(ctx) 95 | 96 | // Handle this error in a sensible manner where possible 97 | defer func() { _ = pusher.Stop(ctx) }() 98 | 99 | // Set global options 100 | otel.SetTracerProvider(tp) 101 | global.SetMeterProvider(pusher.MeterProvider()) 102 | propagator := propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{}) 103 | otel.SetTextMapPropagator(propagator) 104 | 105 | // Sample metric instruments 106 | fooKey := attribute.Key("ex.com/foo") 107 | barKey := attribute.Key("ex.com/bar") 108 | lemonsKey := attribute.Key("ex.com/lemons") 109 | anotherKey := attribute.Key("ex.com/another") 110 | 111 | commonLabels := []attribute.KeyValue{lemonsKey.Int(10), attribute.String("A", "1"), attribute.String("B", "2"), attribute.String("C", "3")} 112 | 113 | meter := global.Meter("ex.com/basic") 114 | 115 | observerCallback := func(_ context.Context, result metric.Float64ObserverResult) { 116 | result.Observe(1, commonLabels...) 117 | } 118 | _ = metric.Must(meter).NewFloat64ValueObserver("ex.com.one", observerCallback, 119 | metric.WithDescription("A ValueObserver set to 1.0"), 120 | ) 121 | 122 | valueRecorder := metric.Must(meter).NewFloat64ValueRecorder("ex.com.two") 123 | 124 | boundRecorder := valueRecorder.Bind(commonLabels...) 125 | defer boundRecorder.Unbind() 126 | 127 | // Create a trace and some measurements 128 | tracer := otel.Tracer("ex.com/basic") 129 | ctx = baggage.ContextWithValues(ctx, 130 | fooKey.String("foo1"), 131 | barKey.String("bar1"), 132 | ) 133 | 134 | func(ctx context.Context) { 135 | var span trace.Span 136 | ctx, span = tracer.Start(ctx, "operation", 137 | trace.WithSpanKind(trace.SpanKindServer)) 138 | defer span.End() 139 | 140 | span.AddEvent("Nice operation!", trace.WithAttributes(attribute.Int("bogons", 100))) 141 | span.SetAttributes(anotherKey.String("yes")) 142 | 143 | meter.RecordBatch( 144 | // Note: call-site variables added as context Entries: 145 | baggage.ContextWithValues(ctx, anotherKey.String("xyz")), 146 | commonLabels, 147 | 148 | valueRecorder.Measurement(2.0), 149 | ) 150 | 151 | func(ctx context.Context) { 152 | var span trace.Span 153 | ctx, span = tracer.Start(ctx, "Sub operation...") 154 | defer span.End() 155 | 156 | span.SetAttributes(lemonsKey.String("five")) 157 | span.AddEvent("Sub span event") 158 | boundRecorder.Record(ctx, 1.3) 159 | }(ctx) 160 | }(ctx) 161 | 162 | } 163 | -------------------------------------------------------------------------------- /examples/simple/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newrelic/opentelemetry-exporter-go/1da00e72320c4267451ae0fa48eba34bf1a08459/examples/simple/screenshot01.png -------------------------------------------------------------------------------- /examples/simple/screenshot02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newrelic/opentelemetry-exporter-go/1da00e72320c4267451ae0fa48eba34bf1a08459/examples/simple/screenshot02.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/newrelic/opentelemetry-exporter-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/newrelic/newrelic-telemetry-sdk-go v0.7.1 7 | go.opentelemetry.io/otel v0.20.0 8 | go.opentelemetry.io/otel/metric v0.20.0 9 | go.opentelemetry.io/otel/sdk v0.20.0 10 | go.opentelemetry.io/otel/sdk/export/metric v0.20.0 11 | go.opentelemetry.io/otel/sdk/metric v0.20.0 12 | go.opentelemetry.io/otel/trace v0.20.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= 2 | github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 6 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/newrelic/newrelic-telemetry-sdk-go v0.7.1 h1:QXmERkem5rwMxrHPVHogvuflpRaw72Lnfl/knHuyQ0E= 8 | github.com/newrelic/newrelic-telemetry-sdk-go v0.7.1/go.mod h1:2kY6OeOxrJ+RIQlVjWDc/pZlT3MIf30prs6drzMfJ6E= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 13 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= 15 | go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= 16 | go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= 17 | go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= 18 | go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= 19 | go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= 20 | go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8= 21 | go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= 22 | go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g= 23 | go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= 24 | go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8= 25 | go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= 26 | go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= 27 | go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= 28 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 29 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /newrelic/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package newrelic_test 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 13 | "github.com/newrelic/opentelemetry-exporter-go/newrelic" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/attribute" 16 | "go.opentelemetry.io/otel/metric/global" 17 | controller "go.opentelemetry.io/otel/sdk/metric/controller/basic" 18 | "go.opentelemetry.io/otel/sdk/resource" 19 | "go.opentelemetry.io/otel/sdk/trace" 20 | "go.opentelemetry.io/otel/semconv" 21 | ) 22 | 23 | func ExampleNewExporter() { 24 | // To enable Infinite Tracing on the New Relic Edge, use the 25 | // telemetry.ConfigSpansURLOverride along with the URL for your Trace 26 | // Observer, including scheme and path. See 27 | // https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing 28 | exporter, err := newrelic.NewExporter( 29 | "My Service", os.Getenv("NEW_RELIC_API_KEY"), 30 | telemetry.ConfigSpansURLOverride("https://nr-internal.aws-us-east-1.tracing.edge.nr-data.net/trace/v1"), 31 | ) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | otel.SetTracerProvider( 36 | trace.NewTracerProvider(trace.WithSyncer(exporter)), 37 | ) 38 | } 39 | 40 | func ExampleNewExportPipeline() { 41 | // Include environment in resource. 42 | r := resource.NewWithAttributes( 43 | attribute.String("environment", "production"), 44 | semconv.ServiceNameKey.String("My Service"), 45 | ) 46 | 47 | // Assumes the NEW_RELIC_API_KEY environment variable contains your New 48 | // Relic Event API key. This will error if it does not. 49 | traceProvider, controller, err := newrelic.NewExportPipeline( 50 | "My Service", 51 | []trace.TracerProviderOption{ 52 | // Conservative sampler. 53 | trace.WithSampler(trace.NeverSample()), 54 | // Reduce span events. 55 | trace.WithSpanLimits(trace.SpanLimits{ 56 | EventCountLimit: 10, 57 | }), 58 | trace.WithResource(r), 59 | }, 60 | []controller.Option{ 61 | // Increase push frequency. 62 | controller.WithCollectPeriod(time.Second), 63 | controller.WithResource(r), 64 | }, 65 | ) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | defer controller.Stop(context.Background()) 70 | 71 | otel.SetTracerProvider(traceProvider) 72 | global.SetMeterProvider(controller.MeterProvider()) 73 | } 74 | 75 | func ExampleInstallNewPipeline() { 76 | // Assumes the NEW_RELIC_API_KEY environment variable contains your New 77 | // Relic Event API key. This will error if it does not. 78 | controller, err := newrelic.InstallNewPipeline("My Service") 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | defer controller.Stop(context.Background()) 83 | } 84 | -------------------------------------------------------------------------------- /newrelic/exporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package newrelic provides an OpenTelemetry exporter for New Relic. 5 | package newrelic 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "os" 12 | "strings" 13 | 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/metric" 16 | "go.opentelemetry.io/otel/metric/global" 17 | "go.opentelemetry.io/otel/sdk/export/metric/aggregation" 18 | controller "go.opentelemetry.io/otel/sdk/metric/controller/basic" 19 | processor "go.opentelemetry.io/otel/sdk/metric/processor/basic" 20 | "go.opentelemetry.io/otel/sdk/metric/selector/simple" 21 | "go.opentelemetry.io/otel/sdk/resource" 22 | "go.opentelemetry.io/otel/semconv" 23 | "go.opentelemetry.io/otel/trace" 24 | 25 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 26 | "github.com/newrelic/opentelemetry-exporter-go/newrelic/internal/transform" 27 | exportmetric "go.opentelemetry.io/otel/sdk/export/metric" 28 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 29 | ) 30 | 31 | const ( 32 | version = "0.20.0" 33 | userAgentProduct = "NewRelic-Go-OpenTelemetry" 34 | ) 35 | 36 | // Exporter exports OpenTelemetry data to New Relic. 37 | type Exporter struct { 38 | harvester *telemetry.Harvester 39 | // serviceName is the name of this service or application. 40 | serviceName string 41 | } 42 | 43 | var ( 44 | errServiceNameEmpty = errors.New("service name is required") 45 | ) 46 | 47 | // NewExporter creates a new Exporter that exports telemetry to New Relic. 48 | func NewExporter(service, apiKey string, options ...func(*telemetry.Config)) (*Exporter, error) { 49 | if service == "" { 50 | return nil, errServiceNameEmpty 51 | } 52 | options = append([]func(*telemetry.Config){ 53 | func(cfg *telemetry.Config) { 54 | cfg.Product = userAgentProduct 55 | cfg.ProductVersion = version 56 | }, 57 | telemetry.ConfigAPIKey(apiKey), 58 | }, options...) 59 | h, err := telemetry.NewHarvester(options...) 60 | if nil != err { 61 | return nil, err 62 | } 63 | return &Exporter{ 64 | harvester: h, 65 | serviceName: service, 66 | }, nil 67 | } 68 | 69 | // NewExportPipeline creates a new OpenTelemetry telemetry pipeline using a 70 | // New Relic Exporter configured with default setting. It is the caller's 71 | // responsibility to stop the returned OTel Controller. This function uses the 72 | // following environment variables to configure the exporter installed in the 73 | // pipeline: 74 | // 75 | // * `NEW_RELIC_API_KEY`: New Relic Event API key. 76 | // * `NEW_RELIC_METRIC_URL`: Override URL to New Relic metric endpoint. 77 | // * `NEW_RELIC_TRACE_URL`: Override URL to New Relic trace endpoint. 78 | // 79 | // More information about the New Relic Event API key can be found 80 | // here: https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#event-insert-key. 81 | // 82 | // The exporter will send telemetry to the default New Relic metric and trace 83 | // API endpoints in the United States. These can be overwritten with the above 84 | // environment variables. These are useful if you wish to send to our EU 85 | // endpoints: 86 | // 87 | // * EU metric API endpoint: metric-api.eu.newrelic.com/metric/v1 88 | // * EU trace API endpoint: trace-api.eu.newrelic.com/trace/v1 89 | func NewExportPipeline(service string, traceOpt []sdktrace.TracerProviderOption, cOpt []controller.Option) (trace.TracerProvider, *controller.Controller, error) { 90 | apiKey, ok := os.LookupEnv("NEW_RELIC_API_KEY") 91 | if !ok { 92 | return nil, nil, errors.New("missing New Relic API key") 93 | } 94 | 95 | var eOpts []func(*telemetry.Config) 96 | if u, ok := os.LookupEnv("NEW_RELIC_METRIC_URL"); ok { 97 | eOpts = append(eOpts, func(cfg *telemetry.Config) { 98 | cfg.MetricsURLOverride = u 99 | }) 100 | } 101 | if u, ok := os.LookupEnv("NEW_RELIC_TRACE_URL"); ok { 102 | eOpts = append(eOpts, telemetry.ConfigSpansURLOverride(u)) 103 | } 104 | 105 | exporter, err := NewExporter(service, apiKey, eOpts...) 106 | if err != nil { 107 | return nil, nil, err 108 | } 109 | 110 | // Minimally default resource with a service name. This is overwritten if 111 | // another is passed in traceOpt or pushOpt. 112 | r := resource.NewWithAttributes(semconv.ServiceNameKey.String(service)) 113 | 114 | tp := sdktrace.NewTracerProvider( 115 | append([]sdktrace.TracerProviderOption{ 116 | sdktrace.WithSyncer(exporter), 117 | sdktrace.WithResource(r), 118 | }, 119 | traceOpt...)..., 120 | ) 121 | 122 | pusher := controller.New( 123 | processor.New( 124 | simple.NewWithExactDistribution(), 125 | exporter, 126 | ), 127 | append([]controller.Option{controller.WithResource(r)}, cOpt...)..., 128 | ) 129 | pusher.Start(context.TODO()) 130 | 131 | return tp, pusher, nil 132 | } 133 | 134 | // InstallNewPipeline installs a New Relic exporter with default settings 135 | // in the global OpenTelemetry telemetry pipeline. It is the caller's 136 | // responsibility to stop the returned push Controller. 137 | // ## Prerequisites 138 | // For details, check out the "Get Started" section of [New Relic Go OpenTelemetry exporter](https://github.com/newrelic/opentelemetry-exporter-go/blob/master/README.md#get-started). 139 | // ## Environment variables 140 | // This function uses the following environment variables to configure 141 | // the exporter installed in the pipeline: 142 | // * `NEW_RELIC_API_KEY`: New Relic Insights insert key. 143 | // * `NEW_RELIC_METRIC_URL`: Override URL to New Relic metric endpoint. 144 | // * `NEW_RELIC_TRACE_URL`: Override URL to New Relic trace endpoint. 145 | // The exporter will send telemetry to the default New Relic metric and trace 146 | // API endpoints in the United States: 147 | // * Traces: https://trace-api.newrelic.com/trace/v1 148 | // * Metrics: https://metric-api.newrelic.com/metric/v1 149 | // You can overwrite these with the above environment variables 150 | // to send data to our EU endpoints or to set up Infinite Tracing. 151 | // For information about changing endpoints, see [OpenTelemetry: Advanced configuration](https://docs.newrelic.com/docs/integrations/open-source-telemetry-integrations/opentelemetry/opentelemetry-advanced-configuration#h2-change-endpoints). 152 | 153 | func InstallNewPipeline(service string) (*controller.Controller, error) { 154 | tp, controller, err := NewExportPipeline(service, nil, nil) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | otel.SetTracerProvider(tp) 160 | global.SetMeterProvider(controller.MeterProvider()) 161 | return controller, nil 162 | } 163 | 164 | var ( 165 | _ sdktrace.SpanExporter = (*Exporter)(nil) 166 | _ exportmetric.Exporter = (*Exporter)(nil) 167 | ) 168 | 169 | // ExportSpans exports span data to New Relic. 170 | func (e *Exporter) ExportSpans(ctx context.Context, spans []*sdktrace.SpanSnapshot) error { 171 | if nil == e { 172 | return nil 173 | } 174 | 175 | var errs []string 176 | for _, s := range spans { 177 | if err := e.harvester.RecordSpan(transform.Span(e.serviceName, s)); err != nil { 178 | errs = append(errs, err.Error()) 179 | } 180 | } 181 | 182 | if len(errs) > 0 { 183 | return fmt.Errorf("export span: %s", strings.Join(errs, ", ")) 184 | } 185 | return nil 186 | } 187 | 188 | // Export exports metrics to New Relic. 189 | func (e *Exporter) Export(_ context.Context, cps exportmetric.CheckpointSet) error { 190 | return cps.ForEach(e, func(record exportmetric.Record) error { 191 | m, err := transform.Record(e.serviceName, record) 192 | if err != nil { 193 | return err 194 | } 195 | e.harvester.RecordMetric(m) 196 | return nil 197 | }) 198 | } 199 | 200 | func (e *Exporter) ExportKindFor(_ *metric.Descriptor, _ aggregation.Kind) exportmetric.ExportKind { 201 | return exportmetric.DeltaExportKind 202 | } 203 | 204 | func (e *Exporter) Shutdown(ctx context.Context) error { 205 | e.harvester.HarvestNow(ctx) 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /newrelic/exporter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package newrelic 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "context" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io/ioutil" 14 | "net/http" 15 | "os" 16 | "testing" 17 | "time" 18 | 19 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 20 | "go.opentelemetry.io/otel/attribute" 21 | "go.opentelemetry.io/otel/metric" 22 | "go.opentelemetry.io/otel/metric/number" 23 | controller "go.opentelemetry.io/otel/sdk/metric/controller/basic" 24 | processor "go.opentelemetry.io/otel/sdk/metric/processor/basic" 25 | selector "go.opentelemetry.io/otel/sdk/metric/selector/simple" 26 | "go.opentelemetry.io/otel/sdk/resource" 27 | "go.opentelemetry.io/otel/sdk/trace" 28 | "go.opentelemetry.io/otel/semconv" 29 | ) 30 | 31 | func TestServiceNameMissing(t *testing.T) { 32 | e, err := NewExporter("", "apiKey") 33 | if e != nil { 34 | t.Error(e) 35 | } 36 | if err != errServiceNameEmpty { 37 | t.Error(err) 38 | } 39 | } 40 | 41 | func TestNilExporter(t *testing.T) { 42 | span := &trace.SpanSnapshot{} 43 | var e *Exporter 44 | 45 | e.ExportSpans(context.Background(), []*trace.SpanSnapshot{span}) 46 | } 47 | 48 | // MockTransport caches decompressed request bodies 49 | type MockTransport struct { 50 | Data []Data 51 | } 52 | 53 | func (c *MockTransport) Spans() []Span { 54 | var spans []Span 55 | for _, data := range c.Data { 56 | spans = append(spans, data.Spans...) 57 | } 58 | return spans 59 | } 60 | 61 | func (c *MockTransport) Metrics() []Metric { 62 | var metrics []Metric 63 | for _, data := range c.Data { 64 | metrics = append(metrics, data.Metrics...) 65 | } 66 | return metrics 67 | } 68 | 69 | func (c *MockTransport) RoundTrip(r *http.Request) (*http.Response, error) { 70 | // telemetry sdk gzip compresses json payloads 71 | gz, err := gzip.NewReader(r.Body) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer gz.Close() 76 | 77 | contents, err := ioutil.ReadAll(gz) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if !json.Valid(contents) { 83 | return nil, errors.New("error validating request body json") 84 | } 85 | err = c.ParseRequest(contents) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return &http.Response{ 91 | StatusCode: 200, 92 | Body: ioutil.NopCloser(&bytes.Buffer{}), 93 | }, nil 94 | } 95 | 96 | func (c *MockTransport) ParseRequest(b []byte) error { 97 | var data []Data 98 | if err := json.Unmarshal(b, &data); err != nil { 99 | return err 100 | } 101 | c.Data = append(c.Data, data...) 102 | return nil 103 | } 104 | 105 | type Data struct { 106 | Common Common `json:"common"` 107 | Spans []Span `json:"spans"` 108 | Metrics []Metric `json:"metrics"` 109 | XXX map[string]interface{} `json:"-"` 110 | } 111 | 112 | type Common struct { 113 | timestamp interface{} 114 | interval interface{} 115 | Attributes map[string]string `json:"attributes"` 116 | } 117 | 118 | type Span struct { 119 | ID string `json:"id"` 120 | TraceID string `json:"trace.id"` 121 | Attributes map[string]interface{} `json:"attributes"` 122 | timestamp interface{} 123 | } 124 | 125 | type Metric struct { 126 | Name string `json:"name"` 127 | Type string `json:"type"` 128 | Value interface{} `json:"value"` 129 | timestamp interface{} 130 | Attributes map[string]interface{} `json:"attributes"` 131 | } 132 | 133 | func TestEndToEndTracer(t *testing.T) { 134 | numSpans := 4 135 | serviceName := "opentelemetry-service" 136 | mockt := &MockTransport{ 137 | Data: make([]Data, 0, numSpans), 138 | } 139 | e, err := NewExporter( 140 | serviceName, 141 | "apiKey", 142 | telemetry.ConfigHarvestPeriod(0), 143 | telemetry.ConfigBasicErrorLogger(os.Stderr), 144 | telemetry.ConfigBasicDebugLogger(os.Stderr), 145 | telemetry.ConfigBasicAuditLogger(os.Stderr), 146 | func(cfg *telemetry.Config) { 147 | cfg.MetricsURLOverride = "localhost" 148 | cfg.SpansURLOverride = "localhost" 149 | cfg.Client.Transport = mockt 150 | }, 151 | ) 152 | if err != nil { 153 | t.Fatalf("failed to instantiate exporter: %v", err) 154 | } 155 | 156 | r := resource.NewWithAttributes(semconv.ServiceNameKey.String(serviceName)) 157 | tracerProvider := trace.NewTracerProvider( 158 | trace.WithBatcher(e, trace.WithBatchTimeout(15), trace.WithMaxExportBatchSize(10)), 159 | trace.WithResource(r), 160 | ) 161 | 162 | tracer := tracerProvider.Tracer("test-tracer") 163 | 164 | var descend func(context.Context, int) 165 | descend = func(ctx context.Context, n int) { 166 | if n <= 0 { 167 | return 168 | } 169 | depth := numSpans - n 170 | ctx, span := tracer.Start(ctx, fmt.Sprintf("Span %d", depth)) 171 | span.SetAttributes(attribute.Int("depth", depth)) 172 | descend(ctx, n-1) 173 | span.End() 174 | } 175 | descend(context.Background(), numSpans) 176 | 177 | // Wait >2 cycles. 178 | <-time.After(40 * time.Millisecond) 179 | e.harvester.HarvestNow(context.Background()) 180 | 181 | gotSpans := mockt.Spans() 182 | if got := len(gotSpans); got != numSpans { 183 | t.Fatalf("expecting %d spans, got %d", numSpans, got) 184 | } 185 | 186 | var traceID, parentID string 187 | // Reverse order to start at the beginning of the trace. 188 | for i := len(gotSpans) - 1; i >= 0; i-- { 189 | depth := numSpans - i - 1 190 | s := gotSpans[i] 191 | name := s.Attributes["name"] 192 | if traceID != "" { 193 | if got := s.TraceID; got != traceID { 194 | t.Errorf("span trace ID for %s: got %q, want %q", name, got, traceID) 195 | } 196 | if got := s.Attributes["parent.id"]; got != parentID { 197 | t.Errorf("span parent ID for %s: got %q, want %q", name, got, parentID) 198 | } 199 | parentID = s.ID 200 | } else { 201 | traceID = s.TraceID 202 | parentID = s.ID 203 | } 204 | if got, want := name, fmt.Sprintf("Span %d", depth); got != want { 205 | t.Errorf("span name: got %q, want %q", got, want) 206 | } 207 | if got := s.Attributes["service.name"]; got != serviceName { 208 | t.Errorf("span service name for %s: got %q, want %q", name, got, serviceName) 209 | } 210 | if got := s.Attributes["depth"].(float64); got != float64(depth) { 211 | t.Errorf("span 'depth' for %s: got %g, want %d", name, got, depth) 212 | } 213 | } 214 | } 215 | 216 | func TestEndToEndMeter(t *testing.T) { 217 | serviceName := "opentelemetry-service" 218 | type data struct { 219 | iKind metric.InstrumentKind 220 | nKind number.Kind 221 | val int64 222 | } 223 | instruments := map[string]data{ 224 | "test-int64-counter": {metric.CounterInstrumentKind, number.Int64Kind, 1}, 225 | "test-float64-counter": {metric.CounterInstrumentKind, number.Float64Kind, 1}, 226 | "test-int64-up-down-counter": {metric.UpDownCounterInstrumentKind, number.Int64Kind, 1}, 227 | "test-float64-up-down-counter": {metric.UpDownCounterInstrumentKind, number.Float64Kind, 1}, 228 | "test-int64-measure": {metric.ValueRecorderInstrumentKind, number.Int64Kind, 2}, 229 | "test-float64-measure": {metric.ValueRecorderInstrumentKind, number.Float64Kind, 2}, 230 | "test-int64-observer": {metric.ValueObserverInstrumentKind, number.Int64Kind, 3}, 231 | "test-float64-observer": {metric.ValueObserverInstrumentKind, number.Float64Kind, 3}, 232 | "test-int64-sum-observer": {metric.SumObserverInstrumentKind, number.Int64Kind, 3}, 233 | "test-float64-sum-observer": {metric.SumObserverInstrumentKind, number.Float64Kind, 3}, 234 | "test-int64-up-down-sum-observer": {metric.UpDownSumObserverInstrumentKind, number.Int64Kind, 3}, 235 | "test-float64-up-down-sum-observer": {metric.UpDownSumObserverInstrumentKind, number.Float64Kind, 3}, 236 | } 237 | 238 | mockt := &MockTransport{ 239 | Data: make([]Data, 0, len(instruments)), 240 | } 241 | exp, err := NewExporter( 242 | serviceName, 243 | "apiKey", 244 | telemetry.ConfigHarvestPeriod(0), 245 | telemetry.ConfigBasicErrorLogger(os.Stderr), 246 | telemetry.ConfigBasicDebugLogger(os.Stderr), 247 | telemetry.ConfigBasicAuditLogger(os.Stderr), 248 | func(cfg *telemetry.Config) { 249 | cfg.MetricsURLOverride = "localhost" 250 | cfg.SpansURLOverride = "localhost" 251 | cfg.Client.Transport = mockt 252 | }, 253 | ) 254 | if err != nil { 255 | t.Fatalf("failed to instantiate exporter: %v", err) 256 | } 257 | 258 | ctx := context.Background() 259 | control := controller.New( 260 | processor.New( 261 | selector.NewWithInexpensiveDistribution(), 262 | exp, // passed as an ExportKindSelector. 263 | ), 264 | // Set collection period longer than this test will run for. 265 | controller.WithCollectPeriod(10*time.Second), 266 | controller.WithPushTimeout(time.Millisecond), 267 | controller.WithExporter(exp), 268 | ) 269 | 270 | if err := control.Start(ctx); err != nil { 271 | t.Fatalf("starting controller: %v", err) 272 | } 273 | 274 | meter := control.MeterProvider().Meter("test-meter") 275 | 276 | newInt64ObserverCallback := func(v int64) metric.Int64ObserverFunc { 277 | return func(ctx context.Context, result metric.Int64ObserverResult) { result.Observe(v) } 278 | } 279 | newFloat64ObserverCallback := func(v float64) metric.Float64ObserverFunc { 280 | return func(ctx context.Context, result metric.Float64ObserverResult) { result.Observe(v) } 281 | } 282 | 283 | for name, data := range instruments { 284 | switch data.iKind { 285 | case metric.CounterInstrumentKind: 286 | switch data.nKind { 287 | case number.Int64Kind: 288 | metric.Must(meter).NewInt64Counter(name).Add(ctx, data.val) 289 | case number.Float64Kind: 290 | metric.Must(meter).NewFloat64Counter(name).Add(ctx, float64(data.val)) 291 | default: 292 | t.Fatal("unsupported number testing kind", data.nKind.String()) 293 | } 294 | case metric.UpDownCounterInstrumentKind: 295 | switch data.nKind { 296 | case number.Int64Kind: 297 | metric.Must(meter).NewInt64UpDownCounter(name).Add(ctx, data.val) 298 | case number.Float64Kind: 299 | metric.Must(meter).NewFloat64UpDownCounter(name).Add(ctx, float64(data.val)) 300 | default: 301 | t.Fatal("unsupported number testing kind", data.nKind.String()) 302 | } 303 | case metric.ValueRecorderInstrumentKind: 304 | switch data.nKind { 305 | case number.Int64Kind: 306 | metric.Must(meter).NewInt64ValueRecorder(name).Record(ctx, data.val) 307 | case number.Float64Kind: 308 | metric.Must(meter).NewFloat64ValueRecorder(name).Record(ctx, float64(data.val)) 309 | default: 310 | t.Fatal("unsupported number testing kind", data.nKind.String()) 311 | } 312 | case metric.ValueObserverInstrumentKind: 313 | switch data.nKind { 314 | case number.Int64Kind: 315 | metric.Must(meter).NewInt64ValueObserver(name, newInt64ObserverCallback(data.val)) 316 | case number.Float64Kind: 317 | metric.Must(meter).NewFloat64ValueObserver(name, newFloat64ObserverCallback(float64(data.val))) 318 | default: 319 | t.Fatal("unsupported number testing kind", data.nKind.String()) 320 | } 321 | case metric.SumObserverInstrumentKind: 322 | switch data.nKind { 323 | case number.Int64Kind: 324 | metric.Must(meter).NewInt64SumObserver(name, newInt64ObserverCallback(data.val)) 325 | case number.Float64Kind: 326 | metric.Must(meter).NewFloat64SumObserver(name, newFloat64ObserverCallback(float64(data.val))) 327 | default: 328 | t.Fatal("unsupported number testing kind", data.nKind.String()) 329 | } 330 | case metric.UpDownSumObserverInstrumentKind: 331 | switch data.nKind { 332 | case number.Int64Kind: 333 | metric.Must(meter).NewInt64UpDownSumObserver(name, newInt64ObserverCallback(data.val)) 334 | case number.Float64Kind: 335 | metric.Must(meter).NewFloat64UpDownSumObserver(name, newFloat64ObserverCallback(float64(data.val))) 336 | default: 337 | t.Fatal("unsupported number testing kind", data.nKind.String()) 338 | } 339 | default: 340 | t.Fatal("unsupported metrics testing kind", data.iKind.String()) 341 | } 342 | } 343 | 344 | // Flush and stop the conroller. 345 | if err := control.Stop(ctx); err != nil { 346 | t.Fatalf("stopping controller: %v", err) 347 | } 348 | 349 | // Flush and stop the exporter. 350 | if err := exp.Shutdown(ctx); err != nil { 351 | t.Fatalf("shutting down exporter: %v", err) 352 | } 353 | 354 | gotMetrics := mockt.Metrics() 355 | if got, want := len(gotMetrics), len(instruments); got != want { 356 | t.Fatalf("expecting %d metrics, got %d", want, got) 357 | } 358 | seen := make(map[string]struct{}, len(instruments)) 359 | for _, m := range gotMetrics { 360 | want, ok := instruments[m.Name] 361 | if !ok { 362 | t.Fatal("unknown metrics", m.Name) 363 | continue 364 | } 365 | seen[m.Name] = struct{}{} 366 | 367 | switch want.iKind { 368 | case metric.CounterInstrumentKind: 369 | if m.Type != "count" { 370 | t.Errorf("metric type for %s: got %q, want \"counter\"", m.Name, m.Type) 371 | continue 372 | } 373 | if got := m.Value.(float64); got != float64(want.val) { 374 | t.Errorf("metric value for %s: got %g, want %d", m.Name, m.Value, want.val) 375 | } 376 | case metric.ValueObserverInstrumentKind: 377 | if m.Type != "gauge" { 378 | t.Errorf("metric type for %s: got %q, want \"gauge\"", m.Name, m.Type) 379 | continue 380 | } 381 | if got := m.Value.(float64); got != float64(want.val) { 382 | t.Errorf("metric value for %s: got %g, want %d", m.Name, m.Value, want.val) 383 | } 384 | case metric.ValueRecorderInstrumentKind: 385 | if m.Type != "summary" { 386 | t.Errorf("metric type for %s: got %q, want \"summary\"", m.Name, m.Type) 387 | continue 388 | } 389 | value := m.Value.(map[string]interface{}) 390 | if got := value["count"].(float64); got != 1 { 391 | t.Errorf("metric value for %s: got %g, want %d", m.Name, m.Value, 1) 392 | } 393 | if got := value["sum"].(float64); got != float64(want.val) { 394 | t.Errorf("metric value for %s: got %g, want %d", m.Name, m.Value, want.val) 395 | } 396 | if got := value["min"].(float64); got != float64(want.val) { 397 | t.Errorf("metric value for %s: got %g, want %d", m.Name, m.Value, want.val) 398 | } 399 | if got := value["max"].(float64); got != float64(want.val) { 400 | t.Errorf("metric value for %s: got %g, want %d", m.Name, m.Value, want.val) 401 | } 402 | } 403 | } 404 | 405 | for i := range instruments { 406 | if _, ok := seen[i]; !ok { 407 | t.Errorf("no metric(s) exported for %q", i) 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /newrelic/internal/transform/identifiers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transform 5 | 6 | const ( 7 | errorCodeAttrKey = "error.code" 8 | errorMessageAttrKey = "error.message" 9 | 10 | serviceNameAttrKey = "service.name" 11 | 12 | instrumentationProviderAttrKey = "instrumentation.provider" 13 | instrumentationProviderAttrValue = "opentelemetry" 14 | 15 | collectorNameAttrKey = "collector.name" 16 | collectorNameAttrValue = "newrelic-opentelemetry-exporter" 17 | ) 18 | -------------------------------------------------------------------------------- /newrelic/internal/transform/metric.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transform 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | 10 | "go.opentelemetry.io/otel/metric/number" 11 | "go.opentelemetry.io/otel/sdk/export/metric/aggregation" 12 | 13 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 14 | "go.opentelemetry.io/otel/attribute" 15 | "go.opentelemetry.io/otel/metric" 16 | metricsdk "go.opentelemetry.io/otel/sdk/export/metric" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | ) 19 | 20 | // ErrUnimplementedAgg is returned when a transformation of an unimplemented 21 | // aggregation is attempted. 22 | var ErrUnimplementedAgg = errors.New("unimplemented aggregation") 23 | 24 | // Record transforms an OpenTelemetry Record into a Metric. 25 | // 26 | // An ErrUnimplementedAgg error is returned for unimplemented Aggregations. 27 | func Record(service string, record metricsdk.Record) (telemetry.Metric, error) { 28 | desc := record.Descriptor() 29 | attrs := attributes(service, record.Resource(), desc, record.Labels()) 30 | switch a := record.Aggregation().(type) { 31 | case aggregation.MinMaxSumCount: 32 | return minMaxSumCount(desc, attrs, a) 33 | case aggregation.Sum: 34 | return sum(desc, attrs, a) 35 | case aggregation.LastValue: 36 | return lastValue(desc, attrs, a) 37 | } 38 | return nil, fmt.Errorf("%w: %T", ErrUnimplementedAgg, record.Aggregation()) 39 | } 40 | 41 | // lastValue transforms a LastValue Aggregation into a Gauge Metric. 42 | func lastValue(desc *metric.Descriptor, attrs map[string]interface{}, a aggregation.LastValue) (telemetry.Metric, error) { 43 | v, t, err := a.LastValue() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return telemetry.Gauge{ 49 | Name: desc.Name(), 50 | Attributes: attrs, 51 | Value: v.CoerceToFloat64(desc.NumberKind()), 52 | Timestamp: t, 53 | }, nil 54 | } 55 | 56 | // sum transforms a Sum Aggregation into a Count Metric. 57 | func sum(desc *metric.Descriptor, attrs map[string]interface{}, a aggregation.Sum) (telemetry.Metric, error) { 58 | sum, err := a.Sum() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return telemetry.Count{ 64 | Name: desc.Name(), 65 | Attributes: attrs, 66 | Value: sum.CoerceToFloat64(desc.NumberKind()), 67 | }, nil 68 | } 69 | 70 | // minMaxSumCountValue returns the values of the MinMaxSumCount Aggregation 71 | // as discret values or any error returned from parsing any of the values. 72 | func minMaxSumCountValues(a aggregation.MinMaxSumCount) (min, max, sum number.Number, count uint64, err error) { 73 | if min, err = a.Min(); err != nil { 74 | return 75 | } 76 | if max, err = a.Max(); err != nil { 77 | return 78 | } 79 | if sum, err = a.Sum(); err != nil { 80 | return 81 | } 82 | if count, err = a.Count(); err != nil { 83 | return 84 | } 85 | return 86 | } 87 | 88 | // minMaxSumCount transforms a MinMaxSumCount Aggregation into a Summary Metric. 89 | func minMaxSumCount(desc *metric.Descriptor, attrs map[string]interface{}, a aggregation.MinMaxSumCount) (telemetry.Metric, error) { 90 | min, max, sum, count, err := minMaxSumCountValues(a) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return telemetry.Summary{ 96 | Name: desc.Name(), 97 | Attributes: attrs, 98 | Count: float64(count), 99 | Sum: sum.CoerceToFloat64(desc.NumberKind()), 100 | Min: min.CoerceToFloat64(desc.NumberKind()), 101 | Max: max.CoerceToFloat64(desc.NumberKind()), 102 | }, nil 103 | } 104 | 105 | func attributes(service string, res *resource.Resource, desc *metric.Descriptor, labels *attribute.Set) map[string]interface{} { 106 | // By default include New Relic attributes and all labels 107 | n := 2 + labels.Len() + res.Len() 108 | if desc != nil { 109 | if desc.Unit() != "" { 110 | n++ 111 | } 112 | if desc.Description() != "" { 113 | n++ 114 | } 115 | } 116 | if service != "" { 117 | n++ 118 | } 119 | attrs := make(map[string]interface{}, n) 120 | 121 | if service != "" { 122 | // This is intentionally overwritten by the resource and then the 123 | // instrument itself if they contain the service name. 124 | attrs[serviceNameAttrKey] = service 125 | } 126 | 127 | for iter := res.Iter(); iter.Next(); { 128 | kv := iter.Label() 129 | attrs[string(kv.Key)] = kv.Value.AsInterface() 130 | } 131 | 132 | // If duplicate labels with Resource these take precedence. 133 | for iter := labels.Iter(); iter.Next(); { 134 | kv := iter.Label() 135 | attrs[string(kv.Key)] = kv.Value.AsInterface() 136 | } 137 | 138 | if desc != nil { 139 | if desc.Unit() != "" { 140 | attrs["unit"] = string(desc.Unit()) 141 | } 142 | if desc.Description() != "" { 143 | attrs["description"] = desc.Description() 144 | } 145 | } 146 | // New Relic registered attributes to identify where this data came from. 147 | attrs[instrumentationProviderAttrKey] = instrumentationProviderAttrValue 148 | attrs[collectorNameAttrKey] = collectorNameAttrValue 149 | 150 | return attrs 151 | } 152 | -------------------------------------------------------------------------------- /newrelic/internal/transform/metric_test.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "go.opentelemetry.io/otel/metric/number" 12 | "go.opentelemetry.io/otel/sdk/export/metric/aggregation" 13 | 14 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 15 | "go.opentelemetry.io/otel/attribute" 16 | "go.opentelemetry.io/otel/metric" 17 | metricsdk "go.opentelemetry.io/otel/sdk/export/metric" 18 | "go.opentelemetry.io/otel/sdk/metric/aggregator/minmaxsumcount" 19 | sumAgg "go.opentelemetry.io/otel/sdk/metric/aggregator/sum" 20 | "go.opentelemetry.io/otel/sdk/resource" 21 | "go.opentelemetry.io/otel/unit" 22 | ) 23 | 24 | var defaultAttrs = map[string]string{ 25 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 26 | collectorNameAttrKey: collectorNameAttrValue, 27 | } 28 | 29 | func TestDefaultAttributes(t *testing.T) { 30 | attrs := attributes("", nil, nil, nil) 31 | if got, want := len(attrs), len(defaultAttrs); got != want { 32 | t.Errorf("incorrect number of default attributes: got %d, want %d", got, want) 33 | } 34 | } 35 | 36 | func TestServiceNameAttributes(t *testing.T) { 37 | wrong := "wrong" 38 | want := "test-service-name" 39 | attrs := attributes(want, nil, nil, nil) 40 | if got, ok := attrs[serviceNameAttrKey]; !ok || got != want { 41 | t.Errorf("service.name attribute wrong: got %q, want %q", got, want) 42 | } 43 | 44 | r := resource.NewWithAttributes(attribute.String("service.name", want)) 45 | attrs = attributes(wrong, r, nil, nil) 46 | if got, ok := attrs[serviceNameAttrKey]; !ok || got != want { 47 | t.Errorf("service.name attribute wrong: got %q, want %q", got, want) 48 | } 49 | 50 | r = resource.NewWithAttributes(attribute.String("service.name", wrong)) 51 | l := attribute.NewSet(attribute.String("service.name", want)) 52 | attrs = attributes(wrong, r, nil, &l) 53 | if got, ok := attrs[serviceNameAttrKey]; !ok || got != want { 54 | t.Errorf("service.name attribute wrong: got %q, want %q", got, want) 55 | } 56 | } 57 | 58 | func TestAttributes(t *testing.T) { 59 | for i, test := range []struct { 60 | res *resource.Resource 61 | opts []metric.InstrumentOption 62 | labels []attribute.KeyValue 63 | want map[string]interface{} 64 | }{ 65 | {}, // test defaults 66 | { 67 | res: resource.NewWithAttributes(attribute.String("A", "a")), 68 | opts: nil, 69 | labels: nil, 70 | want: map[string]interface{}{ 71 | "A": "a", 72 | }, 73 | }, 74 | { 75 | res: resource.NewWithAttributes(attribute.String("A", "a"), attribute.Int64("1", 1)), 76 | opts: nil, 77 | labels: nil, 78 | want: map[string]interface{}{ 79 | "A": "a", 80 | "1": int64(1), 81 | }, 82 | }, 83 | { 84 | res: nil, 85 | opts: []metric.InstrumentOption{metric.WithUnit(unit.Bytes)}, 86 | labels: nil, 87 | want: map[string]interface{}{ 88 | "unit": "By", 89 | }, 90 | }, 91 | { 92 | res: nil, 93 | opts: []metric.InstrumentOption{metric.WithDescription("test description")}, 94 | labels: nil, 95 | want: map[string]interface{}{ 96 | "description": "test description", 97 | }, 98 | }, 99 | { 100 | res: nil, 101 | opts: nil, 102 | labels: []attribute.KeyValue{attribute.String("A", "a")}, 103 | want: map[string]interface{}{ 104 | "A": "a", 105 | }, 106 | }, 107 | { 108 | res: nil, 109 | opts: nil, 110 | labels: []attribute.KeyValue{attribute.String("A", "a"), attribute.Int64("1", 1)}, 111 | want: map[string]interface{}{ 112 | "A": "a", 113 | "1": int64(1), 114 | }, 115 | }, 116 | { 117 | res: resource.NewWithAttributes(attribute.String("K1", "V1"), attribute.String("K2", "V2")), 118 | opts: []metric.InstrumentOption{ 119 | metric.WithUnit(unit.Milliseconds), 120 | metric.WithDescription("d3"), 121 | }, 122 | labels: []attribute.KeyValue{attribute.String("K2", "V3")}, 123 | want: map[string]interface{}{ 124 | "K1": "V1", 125 | "K2": "V3", 126 | "unit": "ms", 127 | "description": "d3", 128 | }, 129 | }, 130 | } { 131 | name := fmt.Sprintf("descriptor test %d", i) 132 | desc := metric.NewDescriptor(name, metric.CounterInstrumentKind, number.Int64Kind, test.opts...) 133 | l := attribute.NewSet(test.labels...) 134 | expected := make(map[string]interface{}, len(defaultAttrs)+len(test.want)) 135 | for k, v := range defaultAttrs { 136 | expected[k] = v 137 | } 138 | for k, v := range test.want { 139 | expected[k] = v 140 | } 141 | got := attributes("", test.res, &desc, &l) 142 | if !reflect.DeepEqual(got, expected) { 143 | t.Errorf("%s: %#v != %#v", name, got, expected) 144 | } 145 | } 146 | } 147 | 148 | var numKinds = []number.Kind{number.Int64Kind, number.Float64Kind} 149 | 150 | func TestMinMaxSumCountRecord(t *testing.T) { 151 | name := "test-mmsc" 152 | l := attribute.NewSet() 153 | for _, iKind := range []metric.InstrumentKind{metric.ValueRecorderInstrumentKind, metric.ValueObserverInstrumentKind} { 154 | for _, nKind := range numKinds { 155 | desc := metric.NewDescriptor(name, iKind, nKind) 156 | alloc := minmaxsumcount.New(2, &desc) 157 | mmsc, ckpt := &alloc[0], &alloc[1] 158 | 159 | var n number.Number 160 | switch nKind { 161 | case number.Int64Kind: 162 | n = number.NewInt64Number(1) 163 | case number.Float64Kind: 164 | n = number.NewFloat64Number(1) 165 | } 166 | if err := mmsc.Update(context.Background(), n, &desc); err != nil { 167 | t.Fatal(err) 168 | } 169 | switch nKind { 170 | case number.Int64Kind: 171 | n = number.NewInt64Number(10) 172 | case number.Float64Kind: 173 | n = number.NewFloat64Number(10) 174 | } 175 | if err := mmsc.Update(context.Background(), n, &desc); err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | if err := mmsc.SynchronizedMove(ckpt, &desc); err != nil { 180 | t.Fatal(err) 181 | } 182 | 183 | m, err := Record("", metricsdk.NewRecord(&desc, &l, nil, ckpt, time.Now(), time.Now())) 184 | if err != nil { 185 | t.Fatalf("Record(MMSC,%s,%s) error: %v", nKind, iKind, err) 186 | } 187 | summary, ok := m.(telemetry.Summary) 188 | if !ok { 189 | t.Fatalf("Record(MMSC,%s,%s) did not return a Summary", nKind, iKind) 190 | } 191 | if got := summary.Name; got != name { 192 | t.Errorf("Record(MMSC,%s,%s) name: got %q, want %q", nKind, iKind, got, name) 193 | } 194 | if want := float64(1); summary.Min != want { 195 | t.Errorf("Record(MMSC,%s,%s) min: got %g, want %g", nKind, iKind, summary.Min, want) 196 | } 197 | if want := float64(10); summary.Max != want { 198 | t.Errorf("Record(MMSC,%s,%s) max: got %g, want %g", nKind, iKind, summary.Max, want) 199 | } 200 | if want := float64(11); summary.Sum != want { 201 | t.Errorf("Record(MMSC,%s,%s) sum: got %g, want %g", nKind, iKind, summary.Sum, want) 202 | } 203 | if want := float64(2); summary.Count != want { 204 | t.Errorf("Record(MMSC,%s,%s) count: got %g, want %g", nKind, iKind, summary.Count, want) 205 | } 206 | } 207 | } 208 | } 209 | 210 | func TestSumRecord(t *testing.T) { 211 | name := "test-sum" 212 | l := attribute.NewSet() 213 | for _, nKind := range numKinds { 214 | desc := metric.NewDescriptor(name, metric.CounterInstrumentKind, nKind) 215 | s := sumAgg.New(1)[0] 216 | 217 | var n number.Number 218 | switch nKind { 219 | case number.Int64Kind: 220 | n = number.NewInt64Number(2) 221 | case number.Float64Kind: 222 | n = number.NewFloat64Number(2) 223 | } 224 | if err := s.Update(context.Background(), n, &desc); err != nil { 225 | t.Fatal(err) 226 | } 227 | 228 | m, err := Record("", metricsdk.NewRecord(&desc, &l, nil, &s, time.Now(), time.Now())) 229 | if err != nil { 230 | t.Fatalf("Record(SUM,%s) error: %v", nKind, err) 231 | } 232 | c, ok := m.(telemetry.Count) 233 | if !ok { 234 | t.Fatalf("Record(SUM,%s) did not return a Counter", nKind) 235 | } 236 | if got := c.Name; got != name { 237 | t.Errorf("Record(SUM,%s) name: got %q, want %q", nKind, got, name) 238 | } 239 | if got := c.Name; got != name { 240 | t.Errorf("Record(SUM) name: got %q, want %q", got, name) 241 | } 242 | if want := float64(2); c.Value != want { 243 | t.Errorf("Record(SUM,%s) value: got %g, want %g", nKind, c.Value, want) 244 | } 245 | } 246 | } 247 | 248 | type fakeAgg struct{} 249 | 250 | func (a fakeAgg) Kind() aggregation.Kind { return aggregation.MinMaxSumCountKind } 251 | func (a fakeAgg) Update(context.Context, number.Number, *metric.Descriptor) error { return nil } 252 | func (a fakeAgg) Checkpoint(context.Context, *metric.Descriptor) {} 253 | func (a fakeAgg) Merge(metricsdk.Aggregator, *metric.Descriptor) error { return nil } 254 | 255 | func TestErrUnimplementedAgg(t *testing.T) { 256 | fa := fakeAgg{} 257 | desc := metric.NewDescriptor("", metric.CounterInstrumentKind, number.Int64Kind) 258 | l := attribute.NewSet() 259 | _, err := Record("", metricsdk.NewRecord(&desc, &l, nil, fa, time.Now(), time.Now())) 260 | if !errors.Is(err, ErrUnimplementedAgg) { 261 | t.Errorf("unexpected error: %v", err) 262 | } 263 | if err == nil { 264 | t.Error("did not get ErrUnimplementedAgg error response") 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /newrelic/internal/transform/span.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transform 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 10 | "go.opentelemetry.io/otel/codes" 11 | "go.opentelemetry.io/otel/sdk/trace" 12 | "go.opentelemetry.io/otel/semconv" 13 | apitrace "go.opentelemetry.io/otel/trace" 14 | ) 15 | 16 | // Span transforms an OpenTelemetry SpanData into a New Relic Span for a 17 | // unique service. 18 | // 19 | // https://godoc.org/github.com/newrelic/newrelic-telemetry-sdk-go/telemetry#Span 20 | // https://godoc.org/go.opentelemetry.io/otel/sdk/export/trace#SpanData 21 | func Span(service string, span *trace.SpanSnapshot) telemetry.Span { 22 | // Default to exporter service name. 23 | serviceName := service 24 | 25 | // Account for the instrumentation provider and collector name. 26 | numAttrs := len(span.Attributes) + span.Resource.Len() + 2 27 | 28 | // If kind has been set, make room for it. 29 | if span.SpanKind != apitrace.SpanKindUnspecified { 30 | numAttrs++ 31 | } 32 | 33 | // Status of Ok and Unset are not considered errors. 34 | isError := span.StatusCode == codes.Error 35 | if isError { 36 | numAttrs += 2 37 | } 38 | 39 | // Copy attributes to new value. 40 | attrs := make(map[string]interface{}, numAttrs) 41 | for iter := span.Resource.Iter(); iter.Next(); { 42 | kv := iter.Label() 43 | // Resource service name overrides the exporter. 44 | if kv.Key == semconv.ServiceNameKey { 45 | serviceName = kv.Value.AsString() 46 | } 47 | attrs[string(kv.Key)] = kv.Value.AsInterface() 48 | } 49 | for _, kv := range span.Attributes { 50 | // Span service name overrides the Resource. 51 | if kv.Key == semconv.ServiceNameKey { 52 | serviceName = kv.Value.AsString() 53 | } 54 | attrs[string(kv.Key)] = kv.Value.AsInterface() 55 | } 56 | 57 | if span.SpanKind != apitrace.SpanKindUnspecified { 58 | attrs["span.kind"] = strings.ToLower(span.SpanKind.String()) 59 | } 60 | 61 | // New Relic registered attributes to identify where this data came from. 62 | attrs[instrumentationProviderAttrKey] = instrumentationProviderAttrValue 63 | attrs[collectorNameAttrKey] = collectorNameAttrValue 64 | 65 | if isError { 66 | attrs[errorCodeAttrKey] = uint32(span.StatusCode) 67 | attrs[errorMessageAttrKey] = span.StatusMessage 68 | } 69 | 70 | parentSpanID := "" 71 | if span.Parent.SpanID().IsValid() { 72 | parentSpanID = span.Parent.SpanID().String() 73 | } 74 | 75 | return telemetry.Span{ 76 | ID: span.SpanContext.SpanID().String(), 77 | TraceID: span.SpanContext.TraceID().String(), 78 | Timestamp: span.StartTime, 79 | Name: span.Name, 80 | ParentID: parentSpanID, 81 | Duration: span.EndTime.Sub(span.StartTime), 82 | ServiceName: serviceName, 83 | Attributes: attrs, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /newrelic/internal/transform/span_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 New Relic Corporation. All rights reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package transform 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" 12 | "go.opentelemetry.io/otel/attribute" 13 | "go.opentelemetry.io/otel/codes" 14 | "go.opentelemetry.io/otel/sdk/resource" 15 | exporttrace "go.opentelemetry.io/otel/sdk/trace" 16 | "go.opentelemetry.io/otel/trace" 17 | ) 18 | 19 | const ( 20 | service = "myService" 21 | sampleTraceIDString = "4bf92f3577b34da6a3ce929d0e0e4736" 22 | sampleSpanIDString = "00f067aa0ba902b7" 23 | sampleParentIDString = "83887e5d7da921ba" 24 | ) 25 | 26 | var ( 27 | sampleTraceID, _ = trace.TraceIDFromHex(sampleTraceIDString) 28 | sampleSpanID, _ = trace.SpanIDFromHex(sampleSpanIDString) 29 | sampleParentID, _ = trace.SpanIDFromHex(sampleParentIDString) 30 | ) 31 | 32 | func TestTransformSpans(t *testing.T) { 33 | now := time.Now() 34 | testcases := []struct { 35 | testname string 36 | input *exporttrace.SpanSnapshot 37 | expect telemetry.Span 38 | }{ 39 | { 40 | testname: "basic span", 41 | input: &exporttrace.SpanSnapshot{ 42 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 43 | TraceID: sampleTraceID, 44 | SpanID: sampleSpanID, 45 | }), 46 | StartTime: now, 47 | EndTime: now.Add(2 * time.Second), 48 | Name: "mySpan", 49 | }, 50 | expect: telemetry.Span{ 51 | Name: "mySpan", 52 | ID: sampleSpanIDString, 53 | TraceID: sampleTraceIDString, 54 | Timestamp: now, 55 | Duration: 2 * time.Second, 56 | ServiceName: service, 57 | Attributes: map[string]interface{}{ 58 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 59 | collectorNameAttrKey: collectorNameAttrValue, 60 | }, 61 | }, 62 | }, 63 | { 64 | testname: "span with parent", 65 | input: &exporttrace.SpanSnapshot{ 66 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 67 | TraceID: sampleTraceID, 68 | SpanID: sampleSpanID, 69 | }), 70 | Parent: trace.NewSpanContext(trace.SpanContextConfig{ 71 | TraceID: sampleTraceID, 72 | SpanID: sampleParentID, 73 | }), 74 | StartTime: now, 75 | EndTime: now.Add(2 * time.Second), 76 | Name: "mySpan", 77 | }, 78 | expect: telemetry.Span{ 79 | Name: "mySpan", 80 | ID: sampleSpanIDString, 81 | TraceID: sampleTraceIDString, 82 | ParentID: sampleParentID.String(), 83 | Timestamp: now, 84 | Duration: 2 * time.Second, 85 | ServiceName: service, 86 | Attributes: map[string]interface{}{ 87 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 88 | collectorNameAttrKey: collectorNameAttrValue, 89 | }, 90 | }, 91 | }, 92 | { 93 | testname: "span with error", 94 | input: &exporttrace.SpanSnapshot{ 95 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 96 | TraceID: sampleTraceID, 97 | SpanID: sampleSpanID, 98 | }), 99 | StatusCode: codes.Error, 100 | StatusMessage: "ResourceExhausted", 101 | StartTime: now, 102 | EndTime: now.Add(2 * time.Second), 103 | Name: "mySpan", 104 | }, 105 | expect: telemetry.Span{ 106 | Name: "mySpan", 107 | ID: sampleSpanIDString, 108 | TraceID: sampleTraceIDString, 109 | Timestamp: now, 110 | Duration: 2 * time.Second, 111 | ServiceName: service, 112 | Attributes: map[string]interface{}{ 113 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 114 | collectorNameAttrKey: collectorNameAttrValue, 115 | errorCodeAttrKey: uint32(codes.Error), 116 | errorMessageAttrKey: "ResourceExhausted", 117 | }, 118 | }, 119 | }, 120 | { 121 | testname: "span with attributes", 122 | input: &exporttrace.SpanSnapshot{ 123 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 124 | TraceID: sampleTraceID, 125 | SpanID: sampleSpanID, 126 | }), 127 | StartTime: now, 128 | EndTime: now.Add(2 * time.Second), 129 | Name: "mySpan", 130 | Attributes: []attribute.KeyValue{ 131 | attribute.Bool("x0", true), 132 | attribute.Float64("x1", 1.0), 133 | attribute.Int("x2", 2), 134 | attribute.Int64("x3", 3), 135 | attribute.String("x4", "4"), 136 | }, 137 | }, 138 | expect: telemetry.Span{ 139 | Name: "mySpan", 140 | ID: sampleSpanIDString, 141 | TraceID: sampleTraceIDString, 142 | Timestamp: now, 143 | Duration: 2 * time.Second, 144 | ServiceName: service, 145 | Attributes: map[string]interface{}{ 146 | "x0": true, 147 | "x1": float64(1.0), 148 | "x2": int64(2), 149 | "x3": int64(3), 150 | "x4": "4", 151 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 152 | collectorNameAttrKey: collectorNameAttrValue, 153 | }, 154 | }, 155 | }, 156 | { 157 | testname: "span with attributes and error", 158 | input: &exporttrace.SpanSnapshot{ 159 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 160 | TraceID: sampleTraceID, 161 | SpanID: sampleSpanID, 162 | }), 163 | StatusCode: codes.Error, 164 | StatusMessage: "ResourceExhausted", 165 | StartTime: now, 166 | EndTime: now.Add(2 * time.Second), 167 | Name: "mySpan", 168 | Attributes: []attribute.KeyValue{ 169 | attribute.Bool("x0", true), 170 | }, 171 | }, 172 | expect: telemetry.Span{ 173 | Name: "mySpan", 174 | ID: sampleSpanIDString, 175 | TraceID: sampleTraceIDString, 176 | Timestamp: now, 177 | Duration: 2 * time.Second, 178 | ServiceName: service, 179 | Attributes: map[string]interface{}{ 180 | "x0": true, 181 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 182 | collectorNameAttrKey: collectorNameAttrValue, 183 | errorCodeAttrKey: uint32(codes.Error), 184 | errorMessageAttrKey: "ResourceExhausted", 185 | }, 186 | }, 187 | }, 188 | { 189 | testname: "span with service name in resource", 190 | input: &exporttrace.SpanSnapshot{ 191 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 192 | TraceID: sampleTraceID, 193 | SpanID: sampleSpanID, 194 | }), 195 | StartTime: now, 196 | EndTime: now.Add(2 * time.Second), 197 | Name: "mySpan", 198 | Resource: resource.NewWithAttributes( 199 | attribute.String("service.name", "resource service"), 200 | ), 201 | }, 202 | expect: telemetry.Span{ 203 | Name: "mySpan", 204 | ID: sampleSpanIDString, 205 | TraceID: sampleTraceIDString, 206 | Timestamp: now, 207 | Duration: 2 * time.Second, 208 | ServiceName: "resource service", 209 | Attributes: map[string]interface{}{ 210 | "service.name": "resource service", 211 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 212 | collectorNameAttrKey: collectorNameAttrValue, 213 | }, 214 | }, 215 | }, 216 | { 217 | testname: "span with a kind", 218 | input: &exporttrace.SpanSnapshot{ 219 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 220 | TraceID: sampleTraceID, 221 | SpanID: sampleSpanID, 222 | }), 223 | SpanKind: trace.SpanKindClient, 224 | StartTime: now, 225 | EndTime: now.Add(2 * time.Second), 226 | Name: "mySpan", 227 | Resource: resource.NewWithAttributes( 228 | attribute.String("service.name", "resource service"), 229 | ), 230 | }, 231 | expect: telemetry.Span{ 232 | Name: "mySpan", 233 | ID: sampleSpanIDString, 234 | TraceID: sampleTraceIDString, 235 | Timestamp: now, 236 | Duration: 2 * time.Second, 237 | ServiceName: "resource service", 238 | Attributes: map[string]interface{}{ 239 | "service.name": "resource service", 240 | "span.kind": "client", 241 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 242 | collectorNameAttrKey: collectorNameAttrValue, 243 | }, 244 | }, 245 | }, 246 | { 247 | testname: "span with service name in attributes", 248 | input: &exporttrace.SpanSnapshot{ 249 | SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ 250 | TraceID: sampleTraceID, 251 | SpanID: sampleSpanID, 252 | }), 253 | StartTime: now, 254 | EndTime: now.Add(2 * time.Second), 255 | Name: "mySpan", 256 | Resource: resource.NewWithAttributes( 257 | attribute.String("service.name", "resource service"), 258 | ), 259 | Attributes: []attribute.KeyValue{ 260 | attribute.String("service.name", "attributes service"), 261 | }, 262 | }, 263 | expect: telemetry.Span{ 264 | Name: "mySpan", 265 | ID: sampleSpanIDString, 266 | TraceID: sampleTraceIDString, 267 | Timestamp: now, 268 | Duration: 2 * time.Second, 269 | ServiceName: "attributes service", 270 | Attributes: map[string]interface{}{ 271 | "service.name": "attributes service", 272 | instrumentationProviderAttrKey: instrumentationProviderAttrValue, 273 | collectorNameAttrKey: collectorNameAttrValue, 274 | }, 275 | }, 276 | }, 277 | } 278 | for _, tc := range testcases { 279 | if got := Span(service, tc.input); !reflect.DeepEqual(got, tc.expect) { 280 | t.Errorf("%s: %#v != %#v", tc.testname, got, tc.expect) 281 | } 282 | } 283 | } 284 | --------------------------------------------------------------------------------