├── .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 | [](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 [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------